كيفية تدريس UITextView لتبرز بشكل جميل

بيان المشكلة


المقدمة: نص متعدد الخطوط.
البحث: خلفية مصممة بشكل جميل.

نعم ، إنها ساعة. "أنت فقط بحاجة إلى وضع backgroundColor في attributesText." لكن هذا لم يكن كافيا. الحقيقة هي أن الاختيار القياسي هو مستطيل ممتلئ. قبيحة. الحل - تحتاج إلى العرف! دفعني الطريق الشائك لإنشائها إلى وصف العملية بحيث لا تضطر الأجيال القادمة إلى المعاناة كثيرًا. مهتمة ، أطلب القط.

قبول


كان الإجراء الأول مكالمة كلاسيكية عبر الإنترنت مع مسألة التنفيذ السليم. ردا على ذلك ، كانت هناك بعض الاقتراحات. بشكل أكثر ملاءمة ، جاء كل شيء لتجاوز طريقة fillBackgroundRectArray. في الحياة الطبيعية ، يكون مسؤولاً عن تلوين النص باستخدام خاصية attributeText المذكورة أعلاه. لمعرفة نوع الحيوان ، جربت حلاً جاهزًا على أمل أن تظل المهمة لبضع ساعات. لم ينجح الأمر. عمل القرار بشكل فظيع.

الوثائق هي كل شيء.


قررت عدم القيام بذلك مرة أخرى ، والتفت إلى الوثائق. من هذا أصبح من الواضح أن UITextView هو سليل بسيط من UIScrollView للنص مدفوعة بواسطة ثلاثة من الصحابة:

  • NSTextStorage - بشكل أساسي برنامج تجميع على NSMutableAtributedString ، مطلوب لتخزين النص وسماته ؛
  • NSTextContainer - NSObject ، المسؤول عن الشكل الهندسي الذي يتم فيه تقديم النص. افتراضيا هو مستطيل ، يمكنك تخصيص أي شيء.
  • NSLayoutManager - يدير الأولين: يأخذ النص ، المسافات البادئة ، يقسم إلى فقرات ، كما أنه مسؤول عن التعبئة التي نحتاجها.

الخوارزميات باردة


نتيجة لذلك ، تتلخص المهمة في إنشاء NSLayoutManadger مخصص وتجاوز الطريقة المطلوبة فيه.

class SelectionLayoutManager: NSLayoutManager { override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) { } 

في التطبيق الرئيسي ، يحصل fillBackgroundRectArray على مستطيلات الكلمات ويرسمها. نظرًا لأن الأرقام الموضحة ، كقاعدة عامة ، هي أجزاء منفصلة من سطر واحد مع تقاطع في أي مكان ، يجب التخلي عنها. ومن هنا جاءت المهمة الفرعية للأوقات: حدد المستطيلات الصحيحة التي تبدأ في بداية السطر ولا يمكن فصلها عن نهايتها.

تتمثل الطريقة التي تعمل على حل هذه المشكلة في الخوارزمية التالية: إنها تدخل في حلقة وفقًا للفقرة وتتحقق مما إذا كان الخط سيتم احتواؤه في سطر واحد إذا تمت إضافة الكلمة التالية إليه؟ إذا لم يكن كذلك ، فهذا خط كامل منفصل ، انتقل إلى التالي. إذا كان الأمر كذلك ، خذ الكلمة التالية وتحقق من الحالة مرة أخرى. نعم ، التنفيذ بسيط للغاية ، كانت هناك فكرة لجعل طريقة تكرارية ، لكن الأيدي لم تصل بعد. في التعليق ، أطلب خياراتك المحسنة.

 private func detectLines(from string: String, width: CGFloat, font: UIFont) -> [String] { var strings: [String] = [] var cumulativeString = "" let words = string.components(separatedBy: CharacterSet.whitespaces) for word in words { let checkingString = cumulativeString + word if checkingString.size(withFont: font).width < width { cumulativeString.append(word) } else { if cumulativeString.isNotEmpty { strings.append(cumulativeString) } if word.size(withFont: font).width < width { cumulativeString = word } else { var stringsFromWord: [String] = [] var handlingWord = word while handlingWord.isNotEmpty { let fullFillString = detectFullFillString(from: handlingWord, width: width, font: font) stringsFromWord.append(fullFillString) handlingWord = word.replacingOccurrences(of: stringsFromWord.reduce("") { $0 + $1 }, with: "") } stringsFromWord.removeLast() strings.append(contentsOf: stringsFromWord) let remainString = word.replacingOccurrences(of: stringsFromWord.reduce("") { $0 + $1 }, with: "") cumulativeString = remainString } } } if cumulativeString.isNotEmpty { strings.append(cumulativeString) } return strings } 

تجدر الإشارة إلى حالة خاصة من الكلمات لا تتناسب في حد ذاتها مع سطر واحد. UITextView يعمل معهم ببساطة شديدة - نقل إلى السطر التالي من الجزء الذي لم يتم تضمينه. نكرر هذا المنطق بطريقة منفصلة مع نفس النهج الخاص بالمرور التسلسلي ، ولكن في هذه الحالة بالرموز. ماذا تحتوي على: سطر كامل ، الباقي - إما سطر كامل ، في حالة كلمة طويلة جدًا ، أو مجرد كلمة جديدة على سطر جديد.

 private func detectFullFillString(from word: String, width: CGFloat, font: UIFont) -> String { var string = "" for character in word { let checkingString = string.appending(String(character)) if checkingString.size(withFont: font).width > width { break } else { string.append(contentsOf: String(character)) } } return string } 

نتيجة لطريقة detectLines (من: width: font :) ، نحصل على مجموعة من السلاسل المقسمة بشكل صحيح على طول الخطوط. بعد ذلك ، من طريقة الإطارات (للخطوط: width: font) ، نحصل على مجموعة من الإحداثيات وأحجام الخطوط.

 private func frames(for lines: [String], width: CGFloat, font: UIFont) -> [CGRect] { var rects: [CGRect] = [] let stringsSizes = lines.map { $0.size(withFont: font) } stringsSizes.forEach { let rect = CGRect(origin: CGPoint(x: (width - $0.width) / 2, y: $0.height * CGFloat(rects.count)), size: $0) rects.append(rect) } return rects } 

جلب الجمال


المهمة الفرعية رقم 2: الطلاء على المستطيلات. تحت مفهوم "جميل" كان من المفترض أن تملأ اللون المحدد للمستطيل مع زوايا التقريب. الحل: ارسم طبقة إضافية عند الإحداثيات المحددة في السياق باستخدام UIBezierPath. لجعل المفاصل تبدو أفضل ، لن ندور حول حواف المستطيل ، وهو أصغر في العرض. الطريقة بسيطة: نحن نتبع إحداثيات كل مستطيل ونرسم الكنتور.

 private func path(from rects: [CGRect], cornerRadius: CGFloat, horizontalInset: CGFloat) -> CGPath { let path = CGMutablePath() rects.enumerated().forEach { (index, rect) in let hasPrevious = index > 0 let isPreviousWider = hasPrevious ? rects[index - 1].width >= rect.width || abs(rects[index - 1].width - rect.width) < 5 : false let hasNext = index != rects.count - 1 let isNextWider = hasNext ? rects[index + 1].width >= rect.width || abs(rects[index + 1].width - rect.width) < 5 : false path.move(to: CGPoint(x: rect.minX - horizontalInset + (isPreviousWider ? 0 : cornerRadius), y: rect.minY)) // top path.addLine(to: CGPoint(x: rect.maxX + horizontalInset - (isPreviousWider ? 0 : cornerRadius), y: rect.minY)) if isPreviousWider == false { path.addQuadCurve(to: CGPoint(x: rect.maxX + horizontalInset, y: rect.minY + cornerRadius), control: CGPoint(x: rect.maxX + horizontalInset, y: rect.minY)) } // right path.addLine(to: CGPoint(x: rect.maxX + horizontalInset, y: rect.maxY - (isNextWider ? 0 : cornerRadius))) if isNextWider == false { path.addQuadCurve(to: CGPoint(x: rect.maxX + horizontalInset - cornerRadius, y: rect.maxY), control: CGPoint(x: rect.maxX + horizontalInset, y: rect.maxY)) } // bottom path.addLine(to: CGPoint(x: rect.minX - horizontalInset + (isNextWider ? 0 : cornerRadius), y: rect.maxY)) if isNextWider == false { path.addQuadCurve(to: CGPoint(x: rect.minX - horizontalInset, y: rect.maxY - cornerRadius), control: CGPoint(x: rect.minX - horizontalInset, y: rect.maxY)) } // left path.addLine(to: CGPoint(x: rect.minX - horizontalInset, y: rect.minY + (isPreviousWider ? 0 : cornerRadius))) if isPreviousWider == false { path.addQuadCurve(to: CGPoint(x: rect.minX - horizontalInset + cornerRadius, y: rect.minY), control: CGPoint(x: rect.minX - horizontalInset, y: rect.minY)) } path.closeSubpath() } return path } 

بعد ذلك ، في طريقة السحب (_: color :) ، املأ المسار.

 private func draw(_ path: CGPath, color: UIColor) { color.set() if let ctx = UIGraphicsGetCurrentContext() { ctx.setAllowsAntialiasing(true) ctx.setShouldAntialias(true) ctx.addPath(path) ctx.drawPath(using: .fillStroke) } } 

الرمز الكامل لطريقة fillBackgroundRectArray (_: count: forCharacterRange: color :)

 override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) { let cornerRadius: CGFloat = 8 let horizontalInset: CGFloat = 5 guard let font = (textStorage?.attributes(at: 0, effectiveRange: nil).first { $0.key == .font })?.value as? UIFont, let textContainerWidth = textContainers.first?.size.width else { return } /// Divide the text into paragraphs let lines = paragraphs(from: textStorage?.string ?? "") /// Divide the paragraphs into separate lines let strings = detectLines(from: lines, width: textContainerWidth, font: font) /// Get rects from the lines let rects = frames(for: strings, width: textContainerWidth, font: font) /// Get a contour by rects let rectsPath = path(from: rects, cornerRadius: cornerRadius, horizontalInset: horizontalInset) /// Fill it draw(rectsPath, color: color) } 

نبدأ. نحن نتحقق. يعمل عظيم.


في الختام: عادة ما تكون العادة صعبة ومميتة ، ولكن كم هي جميلة عندما تعمل كما ينبغي. يلقي ، انه لشيء رائع.

يمكن العثور على الكود الكامل هنا من خلال SelectionLayoutManager .

مراجع


  1. NSLayoutManager
  2. NSTextContainer
  3. NSTextView
  4. استخدام مجموعة نصية لرسم وإدارة النص

Source: https://habr.com/ru/post/ar479992/


All Articles