TextKit框架详细解析 (四) —— 一个简单布局示例(二)

版本记录

版本号 时间
V1.0 2018.08.30

前言

TextKit框架是对Core Text的封装,用简洁的调用方式实现了大部分Core Text的功能。 TextKit是一个偏上层的开发框架,在iOS7以上可用,使用它可以方便灵活处理复杂的文本布局,满足开发中对文本布局的各种复杂需求。TextKit实际上是基于CoreText的一个上层框架,其是面向对象的。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. TextKit框架详细解析 (一) —— 基本概览和应用场景(一)
2. TextKit框架详细解析 (二) —— 基本概览和应用场景(二)
3. TextKit框架详细解析 (三) —— 一个简单布局示例(一)

A UITextView with a Custom Text Kit Stack - 带有自定义文本工具包堆栈的UITextView

从sb编辑器实例化UITextView会自动创建NSTextStorageNSLayoutManagerNSTextContainer(即Text Kit堆栈)的实例,并将所有三个实例公开为只读属性。

没有办法从sb编辑器中更改这些,但幸运的是,如果您以编程方式创建UITextViewText Kit堆栈,则可以。

让我们试一试。 打开Main.storyboard,通过展开Detail Scene / Detail / View并选择Text View来找到NoteEditorViewController视图。 删除此UITextView实例。

接下来,打开NoteEditorViewController.swift并从类中删除UITextView outlet ,并将其替换为以下属性声明:

var textView: UITextView!
var textStorage: SyntaxHighlightTextStorage!

这两个属性适用于文本视图和自定义存储子类。

接下来从viewDidLoad中删除以下行:

textView.text = note.contents
textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

由于您不再使用文本视图的插座,而是手动创建一个插座,因此您不再需要这些线路。

仍在NoteEditorViewController.swift中工作,将以下方法添加到类中:

func createTextView() {
  // 1. Create the text storage that backs the editor
  let attrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
  let attrString = NSAttributedString(string: note.contents, attributes: attrs)
  textStorage = SyntaxHighlightTextStorage()
  textStorage.appendAttributedString(attrString)

  let newTextViewRect = view.bounds

  // 2. Create the layout manager
  let layoutManager = NSLayoutManager()

  // 3. Create a text container
  let containerSize = CGSize(width: newTextViewRect.width, height: CGFloat.max)
  let container = NSTextContainer(size: containerSize)
  container.widthTracksTextView = true
  layoutManager.addTextContainer(container)
  textStorage.addLayoutManager(layoutManager)

  // 4. Create a UITextView
  textView = UITextView(frame: newTextViewRect, textContainer: container)
  textView.delegate = self
  view.addSubview(textView)
}

这是很多代码。 让我们依次考虑每个步骤:

  • 1)实例化自定义文本存储的实例,并使用包含note内容的属性字符串对其进行初始化。
  • 2)创建布局管理器。
  • 3)创建一个文本容器并将其与布局管理器关联。 然后,将布局管理器与文本存储相关联。
  • 4)使用自定义文本容器创建实际文本视图,设置代理,并将文本视图添加为子视图。

此时,前面的图表以及它在四个关键类(storage, layout manager, container and text view)之间显示的关系应该更有意义:

请注意,文本容器的宽度与视图宽度相匹配,但具有无限高度 - 或者尽可能接近CGFLOAT_MAX无穷大。 无论如何,这足以让UITextView滚动并容纳长文本段落。

现在仍然在NoteEditorViewController.swift中工作,直接在viewDidLoad中的super.viewDidLoad()行之后添加以下行:

createTextView()

最后一点:在代码中创建的自定义视图不会自动继承故事板中设置的布局约束;因此,当设备方向更改时,新视图的frame不会调整大小。 您需要自己明确设置frame。

为此,请将以下行添加到viewDidLayoutSubviews的末尾:

textView.frame = view.bounds

构建并运行您的应用程序。打开note并编辑文本,同时密切关注Xcode控制台。 您应该在键入时看到一连串的日志消息,如下所示:

这只是SyntaxHighlightTextStorage中的日志代码,可以指示您实际调用了自定义文本处理代码。

你的文本解析器的基础似乎相当坚实 - 现在添加动态格式!


Dynamic Formatting - 动态格式

在下一步中,您将修改自定义文本存储以包围由星号包围的文本

打开SyntaxHighlightTextStorage.swift并添加以下方法:

func applyStylesToRange(searchRange: NSRange) {
  // 1. create some fonts
  let fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
  let boldFontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
  let boldFont = UIFont(descriptor: boldFontDescriptor, size: 0)
  let normalFont = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

  // 2. match items surrounded by asterisks
  let regexStr = "(\\*\\w+(\\s\\w+)*\\*)"
  let regex = NSRegularExpression(pattern: regexStr, options: nil, error: nil)!
  let boldAttributes = [NSFontAttributeName : boldFont]
  let normalAttributes = [NSFontAttributeName : normalFont]

  // 3. iterate over each match, making the text bold
  regex.enumerateMatchesInString(backingStore.string, options: nil, range: searchRange) {
    match, flags, stop in
    let matchRange = match.rangeAtIndex(1)
    self.addAttributes(boldAttributes, range: matchRange)

    // 4. reset the style to the original
    let maxRange = matchRange.location + matchRange.length
    if maxRange + 1 < self.length {
      self.addAttributes(normalAttributes, range: NSMakeRange(maxRange, 1))
    }
  }
}

上面的代码执行以下操作:

  • 1)创建粗体和普通字体,以使用字体描述符(font descriptors)格式化文本。字体描述符可帮助您避免使用硬编码字体字符串来设置字体类型和样式。
  • 2)创建一个正则表达式(或正则表达式),用于查找由星号包围的任何文本;例如,在字符串“iOS 8 is *awesome* isn't it?”中,存储在上面的regexStr中的正则表达式将匹配并返回文本“* awesome *”。如果你不完全熟悉正则表达式,请不要担心;稍后我们会详细介绍它们。
  • 3)枚举正则表达式返回的匹配项,并将粗体属性应用于每个匹配项。
  • 4)将匹配字符串中最后一个星号后面的字符的文本样式重置为“normal”。这可确保在关闭星号后添加的任何文本都不以粗体显示。

注意:字体描述符是一种描述符语言,允许您通过应用特定属性来修改字体,或获取字体度量的详细信息,而无需实例化UIFont实例。

在上面的代码后面添加以下方法:

func performReplacementsForRange(changedRange: NSRange) {
  var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(changedRange.location, 0)))
  extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(NSMaxRange(changedRange), 0)))
  applyStylesToRange(extendedRange)
}

上面的代码扩展了代码在尝试匹配粗体格式模式时检查的范围。 这是必需的,因为changedRange通常表示单个字符;lineRangeForRange将该范围扩展到整个文本行。

最后,在上面的代码之后添加以下方法:

override func processEditing() {
  performReplacementsForRange(self.editedRange)
  super.processEditing()
}

processEditing发送文本更改为布局管理器的通知。

构建并运行您的应用程序。在note中键入一些文本,并用星号包围一些文本。 文本将自动加粗,如下面的屏幕截图所示:

这非常方便 - 您可能会考虑可能添加到文本中的所有其他样式。


Adding Further Styles - 添加更多样式

将样式应用于分隔文本的基本原则非常简单:使用正则表达式查找和替换分隔字符串,使用applyStylesToRange设置所需的文本样式。

打开SyntaxHighlightTextStorage.swift并将以下方法添加到类中:

func createAttributesForFontStyle(style: String, withTrait trait: UIFontDescriptorSymbolicTraits) -> [NSObject : AnyObject] {
  let fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
  let descriptorWithTrait = fontDescriptor.fontDescriptorWithSymbolicTraits(trait)
  let font = UIFont(descriptor: descriptorWithTrait, size: 0)
  return [NSFontAttributeName : font]
}

此方法将提供的字体样式应用于正文字体。 它为UIFont(descriptor:size :)构造函数提供零大小,强制UIFont返回与用户当前字体大小首选项匹配的大小。

接下来,将以下属性和函数添加到类中:

var replacements: [String : [NSObject : AnyObject]]!

func createHighlightPatterns() {
  let scriptFontDescriptor = UIFontDescriptor(fontAttributes: [UIFontDescriptorFamilyAttribute : "Zapfino"])

  // 1. base our script font on the preferred body font size
  let bodyFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
  let bodyFontSize = bodyFontDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as NSNumber
  let scriptFont = UIFont(descriptor: scriptFontDescriptor, size: CGFloat(bodyFontSize.floatValue))

  // 2. create the attributes
  let boldAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitBold)
  let italicAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitItalic)
  let strikeThroughAttributes = [NSStrikethroughStyleAttributeName : 1]
  let scriptAttributes = [NSFontAttributeName : scriptFont]
  let redTextAttributes = [NSForegroundColorAttributeName : UIColor.redColor()]

  // construct a dictionary of replacements based on regexes
  replacements = [
    "(\\*\\w+(\\s\\w+)*\\*)" : boldAttributes,
    "(_\\w+(\\s\\w+)*_)" : italicAttributes,
    "([0-9]+\\.)\\s" : boldAttributes,
    "(-\\w+(\\s\\w+)*-)" : strikeThroughAttributes,
    "(~\\w+(\\s\\w+)*~)" : scriptAttributes,
    "\\s([A-Z]{2,})\\s" : redTextAttributes
  ]
}

以下是此方法的内容:

  • 首先,使用Zapfino作为字体创建“脚本”样式。 字体描述符有助于确定当前首选的正文字体大小,从而确保脚本字体也符合用户首选的文本大小设置。
  • 接下来,构造要应用于每个匹配样式模式的属性。 您将在稍后看到createAttributesForFontStyle(withTrait :);现在把它停下来。
  • 最后,创建一个将正则表达式映射到上面声明的属性的字典。

如果你对正则表达式不是很熟悉,那么上面的字典可能看起来有点奇怪。 但是如果你逐个解构它包含的正则表达式,你可以不费力地解码它们。

采用上面实现的第一个正则表达式,它匹配星号包围的单词:

(\\*\\w+(\\s\\w+)*\\*)

双斜杠是必须使用额外的反斜杠转义文字字符串中的特殊字符的结果。 如果你抛出转义的反斜杠,并只考虑核心正则表达式,它看起来像这样:

(\*\w+(\s\w+)*\*)

现在,逐步解构正则表达式:

  • 1)(\ *- 匹配星号
  • 2)\ w + - 后跟一个或多个“单词”字符
  • 3)(\ s \ w +)* - 后跟零个或多个空格组,后跟“单词”字符
  • 4)\ *) - 后面跟一个星号

作为练习,使用上面的解释和备忘单作为指导,自己解码其他正则表达式。 你可以自己做多少?

这是给你的问题。 你能用简单的英文描述正则表达式:
\ s([A-Z] {2,})\ s匹配?

由空格包围的两个或多个大写字母组成的任何字符串。

您还需要初始化替换字典。 将以下类初始值设定项添加到SyntaxHighlightTextStorage类:

override init() {
    super.init()
    createHighlightPatterns()
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

您在项目的其余部分中调用了没有参数的普通初始值设定项。

最后,使用以下内容替换applyStylesToRange()的实现:

func applyStylesToRange(searchRange: NSRange) {
  let normalAttrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]

  // iterate over each replacement
  for (pattern, attributes) in replacements {
    let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)!
    regex.enumerateMatchesInString(backingStore.string, options: nil, range: searchRange) {
      match, flags, stop in
      // apply the style
      let matchRange = match.rangeAtIndex(1)
      self.addAttributes(attributes, range: matchRange)

      // reset the style to the original
      let maxRange = matchRange.location + matchRange.length
      if maxRange + 1 < self.length {
        self.addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
      }
    }
  }
}

以前,此方法只执行一次正则表达式搜索粗体文本。 现在它做了同样的事情,但它遍历正则表达式匹配和属性的字典,因为有许多文本样式要查找。 对于每个正则表达式,它运行搜索并将指定的样式应用于匹配的模式。

请注意,NSRegularExpression的初始化可能会失败,因此在此隐式解包。 如果由于某种原因,模式中存在错误导致模式编译失败,则代码将在此行上失败,从而迫使您修复模式,而不是进一步失败。

构建并运行您的应用程序,并运用您可以使用的所有新样式,如下所示:

这是一个更具挑战性的练习。 如果您在注释中输入文本:"*This is not bold*"(不带引号),您会发现它不会变为粗体。 换句话说,如果所选文本在单词之间有多个空格,则不匹配。

你能创建一个会鼓励那个文本的正则表达式吗? 它只是对代码中已经存在的修改的简单修改。

(\*\w+(\s+\w+)*\*) – try it out!

你的应用程序已接近完成;只有一些小问题要处理。

如果您在处理应用程序时更改了屏幕方向,则您已经注意到应用程序不再响应内容大小更改通知,因为您的自定义实现尚不支持此操作。

至于第二个问题,如果你在note中添加了大量文字,你会发现文本视图的底部被键盘部分遮挡了;当你看不到你正在打字的东西时,输入东西有点困难!

是时候解决这两个问题了。


Reviving Dynamic Type - 恢复动态类型

要更正动态类型的问题,您的代码应在内容大小更改通知发生时更新包含note文本的属性字符串使用的字体。

将以下函数添加到SyntaxHighlightTextStorage类:

func update() {
  // update the highlight patterns
  createHighlightPatterns()

  // change the 'global' font
  let bodyFont = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
  addAttributes(bodyFont, range: NSMakeRange(0, length))

  // re-apply the regex matches
  applyStylesToRange(NSMakeRange(0, length))
}

上述方法更新与各种正则表达式关联的所有字体,将正文文本样式应用于整个字符串,然后重新应用高亮样式。

最后,打开NoteEditorViewController.swift并修改preferredContentSizeChanged()以执行更新:

func preferredContentSizeChanged(notification: NSNotification) {
  textStorage.update()
  updateTimeIndicatorFrame()
}

构建并运行,更改文本大小首选项,文本应相应调整,如下例所示:


Resizing Text Views - 调整文本视图的大小

剩下要做的就是解决编辑长notes时键盘遮住文本视图下半部分的问题。 这是iOS 8尚未为我们解决的一个问题!

要解决此问题,您可以在键盘可见时缩小文本视图框的大小。

在调用createTextView()之后立即打开NoteEditorViewController.swift并将以下行添加到viewDidLoad()

textView.scrollEnabled = true

这样可以在注释编辑器视图中滚动文本视图。

现在将以下代码添加到viewDidLoad()的底部:

NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidHide:", name: UIKeyboardDidHideNotification, object: nil)

这会添加键盘显示或隐藏时的通知,这是相应调整文本视图框大小的信号。

接下来,将以下方法添加到类中:

func updateTextViewSizeForKeyboardHeight(keyboardHeight: CGFloat) {
  textView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - keyboardHeight)
}

此方法可降低文本视图的高度以适应键盘。

最后,您需要实现两种方法来响应通知。 将以下方法添加到类中:

func keyboardDidShow(notification: NSNotification) {
  if let rectValue = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue {
    let keyboardSize = rectValue.CGRectValue().size
    updateTextViewSizeForKeyboardHeight(keyboardSize.height)
  }
}

func keyboardDidHide(notification: NSNotification) {
  updateTextViewSizeForKeyboardHeight(0)
}

当显示键盘时,您需要从通知中提取键盘大小,从那里获得键盘高度很简单。 当键盘隐藏时,您只需将高度调整重置为零。

注意:虽然在早期版本的iOS上,您需要在计算新文本视图大小时考虑当前屏幕方向(因为当屏幕方向更改时,UIView实例的宽度和高度属性会被交换,但键盘的宽度和高度属性不会),iOS 8不再需要这个。

构建并运行您的应用程序,编辑note并检查显示键盘不再遮挡文本,如下所示:

注意:在撰写本文时,iOS 8存在一个微妙的错误 - 当文本视图调整大小时,光标位置可能仍然在屏幕外。 如果用户点击“return”键,光标将移动到正确的位置。 我们将密切关注这一点,如果错误持续存在,我们将尝试寻找替代解决方案。


源码

1. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?

  func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {

    // style the navigation bar
    let navColor = UIColor(red: 0.175, green: 0.458, blue: 0.831, alpha: 1.0)
    UINavigationBar.appearance().barTintColor = navColor
    UINavigationBar.appearance().tintColor = UIColor.whiteColor()
    UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName : UIColor.whiteColor()]

    // make the status bar white
    application.statusBarStyle = .LightContent

    return true
  }


}
2. Note.swift
import Foundation

class Note {
  var contents: String
  let timestamp: NSDate

  // an automatically generated note title, based on the first line of the note
  var title: String {
    // split into lines
    let lines = contents.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
    // return the first
    return lines[0]
  }

  init(text: String) {
    contents = text
    timestamp = NSDate()
  }

}


// some default notes to play with
var notes = [
  Note(text: "Shopping List\r\r1. Cheese\r2. Biscuits\r3. Sausages\r4. IMPORTANT Cash for going out!\r5. -potatoes-\r6. A copy of iOS8 by tutorials\r7. A new iPhone\r8. A present for mum"),
  Note(text: "Meeting notes\rA long and drawn out meeting, it lasted hours and hours and hours!"),
  Note(text: "Perfection ... \n\nPerfection is achieved not when there is nothing left to add, but when there is nothing left to take away - Antoine de Saint-Exupery"),
  Note(text: "Notes on Swift\nThis new language from Apple is changing iOS development as we know it!"),
  Note(text: "Meeting notes\rA dfferent meeting, just as long and boring"),
  Note(text: "A collection of thoughts\rWhy do birds sing? Why is the sky blue? Why is it so hard to create good test data?")
]
3. NoteEditorViewController.swift
import UIKit

class NoteEditorViewController: UIViewController, UITextViewDelegate {

  var textView: UITextView!
  var textStorage: SyntaxHighlightTextStorage!

  var note: Note!
  var timeView: TimeIndicatorView!

  override func viewDidLoad() {
    super.viewDidLoad()

    createTextView()
    textView.scrollEnabled = true

    NSNotificationCenter.defaultCenter().addObserver(self,
      selector: "preferredContentSizeChanged:",
      name: UIContentSizeCategoryDidChangeNotification,
      object: nil)

    timeView = TimeIndicatorView(date: note.timestamp)
    textView.addSubview(timeView)

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidHide:", name: UIKeyboardDidHideNotification, object: nil)
  }

  func preferredContentSizeChanged(notification: NSNotification) {
    textStorage.update()
    updateTimeIndicatorFrame()
  }

  func textViewDidEndEditing(textView: UITextView!) {
    note.contents = textView.text
  }

  override func viewDidLayoutSubviews() {
    updateTimeIndicatorFrame()
    textView.frame = view.bounds
  }

  func updateTimeIndicatorFrame() {
    timeView.updateSize()
    timeView.frame = CGRectOffset(timeView.frame, textView.frame.width - timeView.frame.width, 0)

    let exclusionPath = timeView.curvePathWithOrigin(timeView.center)
    textView.textContainer.exclusionPaths = [exclusionPath]
  }

  func createTextView() {
    // 1. Create the text storage that backs the editor
    let attrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
    let attrString = NSAttributedString(string: note.contents, attributes: attrs)
    textStorage = SyntaxHighlightTextStorage()
    textStorage.appendAttributedString(attrString)

    let newTextViewRect = view.bounds

    // 2. Create the layout manager
    let layoutManager = NSLayoutManager()

    // 3. Create a text container
    let containerSize = CGSize(width: newTextViewRect.width, height: CGFloat.max)
    let container = NSTextContainer(size: containerSize)
    container.widthTracksTextView = true
    layoutManager.addTextContainer(container)
    textStorage.addLayoutManager(layoutManager)

    // 4. Create a UITextView
    textView = UITextView(frame: newTextViewRect, textContainer: container)
    textView.delegate = self
    view.addSubview(textView)
  }

  func updateTextViewSizeForKeyboardHeight(keyboardHeight: CGFloat) {
    textView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - keyboardHeight)
  }

  func keyboardDidShow(notification: NSNotification) {
    if let rectValue = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue {
      let keyboardSize = rectValue.CGRectValue().size
      updateTextViewSizeForKeyboardHeight(keyboardSize.height)
    }
  }

  func keyboardDidHide(notification: NSNotification) {
    updateTextViewSizeForKeyboardHeight(0)
  }
}
4. NotesListViewController.swift
import UIKit

class NotesListViewController: UITableViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    NSNotificationCenter.defaultCenter().addObserver(self,
      selector: "preferredContentSizeChanged:",
      name: UIContentSizeCategoryDidChangeNotification,
      object: nil)
  }

  override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    // whenever this view controller appears, reload the table. This allows it to reflect any changes
    // made whilst editing notes
    tableView.reloadData()
  }

  func preferredContentSizeChanged(notification: NSNotification) {
    tableView.reloadData()
  }

  // #pragma mark - Table view data source

  override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
  }

  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return notes.count
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

    let note = notes[indexPath.row]
    let font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
    let textColor = UIColor(red: 0.175, green: 0.458, blue: 0.831, alpha: 1)
    let attributes = [
      NSForegroundColorAttributeName : textColor,
      NSFontAttributeName : font,
      NSTextEffectAttributeName : NSTextEffectLetterpressStyle
    ]
    let attributedString = NSAttributedString(string: note.title, attributes: attributes)

    cell.textLabel?.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)

    cell.textLabel?.attributedText = attributedString
    
    return cell
  }

  let label: UILabel = {
    let temporaryLabel = UILabel(frame: CGRect(x: 0, y: 0, width: Int.max, height: Int.max))
    temporaryLabel.text = "test"
    return temporaryLabel
    }()

  override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    label.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
    label.sizeToFit()
    return label.frame.height * 1.7
  }

  override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
      notes.removeAtIndex(indexPath.row)
      tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    }
  }

  // #pragma mark - Navigation

  // In a storyboard-based application, you will often want to do a little preparation before navigation
  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
    if let editorVC = segue.destinationViewController as? NoteEditorViewController {

      if "CellSelected" == segue.identifier {
        if let path = tableView.indexPathForSelectedRow() {
          editorVC.note = notes[path.row]
        }
      } else if "AddNewNote" == segue.identifier {
        let note = Note(text: " ")
        editorVC.note = note
        notes.append(note)
      }
    }
  }

}
5. TimeIndicatorView.swift
import UIKit

class TimeIndicatorView: UIView {

  var label = UILabel()

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  init(date: NSDate) {
    super.init(frame: CGRectZero)

    // Initialization code
    backgroundColor = UIColor.clearColor()
    clipsToBounds = false

    // format and style the date
    let formatter = NSDateFormatter()
    formatter.dateFormat = "dd\rMMMM\ryyyy"
    let formattedDate = formatter.stringFromDate(date)
    label.text = formattedDate.uppercaseString
    label.textAlignment = .Center
    label.textColor = UIColor.whiteColor()
    label.numberOfLines = 0

    addSubview(label)
  }

  func updateSize() {
    // size the label based on the font
    label.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
    label.frame = CGRect(x: 0, y: 0, width: Int.max, height: Int.max)
    label.sizeToFit()

    // set the frame to be large enough to accomodate the circle that surrounds the text
    let radius = radiusToSurroundFrame(label.frame)
    frame = CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2)

    // center the label within this circle
    label.center = center

    // offset the center of this view to ... erm ... can I just draw you a picture?
    // You know the story - the designer provides a mock-up with some static data, leaving
    // you to work out the complex calculations required to accomodate the variability of real-world
    // data. C'est la vie!
    let padding : CGFloat = 5.0
    center = CGPoint(x: center.x + label.frame.origin.x - padding, y: center.y - label.frame.origin.y + padding)
  }

  // calculates the radius of the circle that surrounds the label
  func radiusToSurroundFrame(frame: CGRect) -> CGFloat {
    return max(frame.width, frame.height) * 0.5 + 25
  }

  func curvePathWithOrigin(origin: CGPoint) -> UIBezierPath {
    return UIBezierPath(arcCenter: origin, radius: radiusToSurroundFrame(label.frame), startAngle: -180, endAngle: 180, clockwise: true)
  }

  override func drawRect(rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    CGContextSetShouldAntialias(context, true)
    let path = curvePathWithOrigin(label.center)
    UIColor(red: 0.329, green: 0.584, blue: 0.898, alpha: 1).setFill()
    path.fill()
  }
}
6. SyntaxHighlightTextStorage.swift
import UIKit

class SyntaxHighlightTextStorage: NSTextStorage {
  let backingStore = NSMutableAttributedString()
  var replacements: [String : [NSObject : AnyObject]]!

  override init() {
    super.init()
    createHighlightPatterns()
  }

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  override var string: String {
    return backingStore.string
  }

  override func attributesAtIndex(index: Int, effectiveRange range: NSRangePointer) -> [NSObject : AnyObject] {
    return backingStore.attributesAtIndex(index, effectiveRange: range)
  }

  override func replaceCharactersInRange(range: NSRange, withString str: String) {
    println("replaceCharactersInRange:\(range) withString:\(str)")

    beginEditing()
    backingStore.replaceCharactersInRange(range, withString:str)
    edited(.EditedCharacters | .EditedAttributes, range: range, changeInLength: (str as NSString).length - range.length)
    endEditing()
  }

  override func setAttributes(attrs: [NSObject : AnyObject]!, range: NSRange) {
    println("setAttributes:\(attrs) range:\(range)")

    beginEditing()
    backingStore.setAttributes(attrs, range: range)
    edited(.EditedAttributes, range: range, changeInLength: 0)
    endEditing()
  }

  func applyStylesToRange(searchRange: NSRange) {
    let normalAttrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]

    // iterate over each replacement
    for (pattern, attributes) in replacements {
      let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)!
      regex.enumerateMatchesInString(backingStore.string, options: nil, range: searchRange) {
        match, flags, stop in
        // apply the style
        let matchRange = match.rangeAtIndex(1)
        self.addAttributes(attributes, range: matchRange)

        // reset the style to the original
        let maxRange = matchRange.location + matchRange.length
        if maxRange + 1 < self.length {
          self.addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
        }
      }
    }
  }

  func performReplacementsForRange(changedRange: NSRange) {
    var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(changedRange.location, 0)))
    extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(NSMaxRange(changedRange), 0)))
    applyStylesToRange(extendedRange)
  }

  override func processEditing() {
    performReplacementsForRange(self.editedRange)
    super.processEditing()
  }

  func createAttributesForFontStyle(style: String, withTrait trait: UIFontDescriptorSymbolicTraits) -> [NSObject : AnyObject] {
    let fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
    let descriptorWithTrait = fontDescriptor.fontDescriptorWithSymbolicTraits(trait)
    let font = UIFont(descriptor: descriptorWithTrait, size: 0)
    return [NSFontAttributeName : font]
  }

  func createHighlightPatterns() {
    let scriptFontDescriptor = UIFontDescriptor(fontAttributes: [UIFontDescriptorFamilyAttribute : "Zapfino"])

    // 1. base our script font on the preferred body font size
    let bodyFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
    let bodyFontSize = bodyFontDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as NSNumber
    let scriptFont = UIFont(descriptor: scriptFontDescriptor, size: CGFloat(bodyFontSize.floatValue))

    // 2. create the attributes
    let boldAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitBold)
    let italicAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitItalic)
    let strikeThroughAttributes = [NSStrikethroughStyleAttributeName : 1]
    let scriptAttributes = [NSFontAttributeName : scriptFont]
    let redTextAttributes = [NSForegroundColorAttributeName : UIColor.redColor()]

    // construct a dictionary of replacements based on regexes
    replacements = [
      "(\\*\\w+(\\s\\w+)*\\*)" : boldAttributes,
      "(_\\w+(\\s\\w+)*_)" : italicAttributes,
      "([0-9]+\\.)\\s" : boldAttributes,
      "(-\\w+(\\s\\w+)*-)" : strikeThroughAttributes,
      "(~\\w+(\\s\\w+)*~)" : scriptAttributes,
      "\\s([A-Z]{2,})\\s" : redTextAttributes
    ]
  }

  func update() {
    // update the highlight patterns
    createHighlightPatterns()

    // change the 'global' font
    let bodyFont = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
    addAttributes(bodyFont, range: NSMakeRange(0, length))

    // re-apply the regex matches
    applyStylesToRange(NSMakeRange(0, length))
  }

}

后记

本篇主要讲述了一个简单布局示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容