بيان المشكلة
المقدمة: نص متعدد الخطوط.
البحث: خلفية مصممة بشكل جميل.
نعم ، إنها ساعة. "أنت فقط بحاجة إلى وضع 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))
بعد ذلك ، في طريقة السحب (_: 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 }
نبدأ. نحن نتحقق. يعمل عظيم.
في الختام: عادة ما تكون العادة صعبة ومميتة ، ولكن كم هي جميلة عندما تعمل كما ينبغي. يلقي ، انه لشيء رائع.
يمكن العثور على الكود الكامل هنا من خلال
SelectionLayoutManager .
مراجع
- NSLayoutManager
- NSTextContainer
- NSTextView
- استخدام مجموعة نصية لرسم وإدارة النص