Slide 1

Slide 1 text

FromAtom ܅͚ͩͷGFMΤσΟλΛ࡞Ζ͏ʂ iOSDC Japan 2023 2023/09/02 16:50~ Track C

Slide 2

Slide 2 text

• ϐΫγϒגࣜձࣾ • iOSΞϓϦ෼໺ςοΫϦʔυ • ޷͖ͳSwift͸ guard 2 @FromAtom

Slide 3

Slide 3 text

ࠓ೔ͷαϯϓϧϦϙδτϦ 3 https://github.com/FromAtom/iosdc-2023-gfm-editor-sample

Slide 4

Slide 4 text

Markdownͷྺ࢙ 4 ͬ͘͟Γ

Slide 5

Slide 5 text

Markdownͱ͸ • 2004೥ʹδϣϯɾάϧʔόʔʹΑͬͯ։ൃ͞Εͨ • ςΩετ͔ΒHTMLʹม׵͢Δ࢓૊ΈʢMarkdown.plʣ 5

Slide 6

Slide 6 text

CommonMarkͱ͸ • Markdown.plΛ֦ுͨ͠ํݴ͕େྔʹੜ·Εͨ ‣ ֦ுྫɿςʔϒϧه๏ɺ٭஫ه๏ • ͦΕΛ౷Ұ͢ΔͨΊʹCommonMark͕ੜ·Εͨ 6

Slide 7

Slide 7 text

GitHub Flavored Markdownͱ͸ʁ • CommonMarkΛ֦ுͨ͠ͷ͕GitHub Flavored MarkdownʢGFMʣ • GitHub্Ͱ࢖͑ΔMarkdownͱࣅͨ΋ͷ • ݫີʹ͸Լهͷ఺͕ҟͳ͍ͬͯΔ ‣ ֆจࣈ (ex. :smile:) ‣ ϝϯγϣϯ ‣ ίϛοτʢίϛοτIDʣɾΠγϡʔɾϦϙδτϦͷࢀর 7

Slide 8

Slide 8 text

ࠓ೔ͷ໨ඪ 8

Slide 9

Slide 9 text

ࠓ೔ͷ໨ඪ • GitHub Flavored MarkdownϕʔεͰ • ೖྗิॿ͕͋ͬͯ • γϯλοΫεϋΠϥΠτ͞ΕΔ • ΤσΟλʔΛ࡞Ζ͏ 9

Slide 10

Slide 10 text

ೖྗิॿΛ࡞Ζ͏ 10

Slide 11

Slide 11 text

ೖྗิॿͱ͸ 11 Markdownه๏Λ؆୯ʹೖྗͰ͖Δ 
 Ϙλϯ΍ิ׬ػೳ

Slide 12

Slide 12 text

͜͏͢ΔͱͰ͖ͦ͏ 12 ΧʔιϧͷҐஔΛऔಘ͢Δ ΧʔιϧҐஔ͕ଠࣈ͔Ͳ͏͔֬ೝ͢Δ ଠࣈͳΒ࡟আɾଠࣈ͡Όͳ͍ͳΒଠࣈه߸Λૠೖ 1 2 3

Slide 13

Slide 13 text

13 ਖ਼نදݱͰ࣮૷Ͱ͖Δͬ͠ΐ

Slide 14

Slide 14 text

14 ͱΓ͋͑ͣ ଠࣈ Ͱ΍ͬͯΈΑ

Slide 15

Slide 15 text

15 **太字** ΍ͬͯΈͨ݁Ռ

Slide 16

Slide 16 text

16 **太字** ΍ͬͯΈͨ݁Ռ • ΧʔιϧͷલޙͰߦΛ෼ׂ

Slide 17

Slide 17 text

17 ΍ͬͯΈͨ݁Ռ **太 字** • ΧʔιϧͷલޙͰߦΛ෼ׂ • ෼ׂޙʹ** Λਖ਼نදݱͰ୳ࠪ

Slide 18

Slide 18 text

18 ΍ͬͯΈͨ݁Ռ • ΧʔιϧͷલޙͰߦΛ෼ׂ • ෼ׂޙʹ** Λਖ਼نදݱͰ୳ࠪ • ୳ࠪʹώοτͨ͠Βଠࣈ൑ఆ **太 字**

Slide 19

Slide 19 text

19 ΍ɺ΍͔ͬͨ……ʁ

Slide 20

Slide 20 text

20 ΍ͬͯΈͨ݁Ռ **太字**太字じゃない**太字**

Slide 21

Slide 21 text

21 ΍ͬͯΈͨ݁Ռ **太字**太字 じゃない**太字**

Slide 22

Slide 22 text

22 ΍ͬͯΈͨ݁Ռ **太字**太字 じゃない**太字**

Slide 23

Slide 23 text

23 ΍ͬͯΈͨ݁Ռ **太字**太字じゃない**太字** ଠࣈ͡Όͳ͍ͷʹଠࣈ൑ఆ͞Εͯ͠·͏

Slide 24

Slide 24 text

24 ͩΊ͡ΌΜ

Slide 25

Slide 25 text

25 ਖ਼نදݱͰ͸͏·͍͔͘ͳ͍ `**コードスパン内なので太字じゃない**`

Slide 26

Slide 26 text

26 ਖ਼نදݱͰ͸͏·͍͔͘ͳ͍ `**コードスパン内なので太字じゃない**` **`コードスパン全体が太字`**

Slide 27

Slide 27 text

27 ਖ਼نදݱͰ͸͏·͍͔͘ͳ͍ `**コードスパン内なので太字じゃない**` **`コードスパン全体が太字`** `**`太字記号がコードスパン内にあるので太字じゃない`**`

Slide 28

Slide 28 text

28 ਖ਼نදݱͰ͸͏·͍͔͘ͳ͍ `**コードスパン内なので太字じゃない**` **`コードスパン全体が太字`** `**`太字記号がコードスパン内にあるので太字じゃない`**` **複数行に またがる 太字**

Slide 29

Slide 29 text

29 ਖ਼نදݱͰ͸ݶք͕͋Δ

Slide 30

Slide 30 text

swift-markdownΛ࢖͓͏ 30

Slide 31

Slide 31 text

swift-markdownͱ͸ʁ • AppleެࣜͷMarkdownύʔβ • cmark-gfmΛར༻͍ͯ͠Δ ‣ ͜ΕΛ࢖͑͹GFMʹରԠͰ͖Δ • https://github.com/apple/swift-markdown 31

Slide 32

Slide 32 text

swift-markdownͱ͸ʁ 32 import Markdown let source = "This is a markup *document*." let document = Document(parsing: source) print(document.debugDescription()) // Document // └─ Paragraph // ├─ Text "This is a markup " // ├─ Emphasis // │ └─ Text "document" // └─ Text "."

Slide 33

Slide 33 text

ೖྗิॿʢଠࣈฤʣ 33 ΧʔιϧͷҐஔΛऔಘ͢Δ ΧʔιϧҐஔ͕ଠࣈ͔Ͳ͏͔֬ೝ͢Δ ଠࣈه߸Λૠೖ or ࡟আ͢Δ 1 2 3 4 ΧʔιϧҐஔΛswift-markdown͕ѻ͑ΔΑ͏ʹม׵͢Δ

Slide 34

Slide 34 text

34 @available(iOS 3.2, *) @NSCopying var selectedTextRange: UITextRange? { get set } open var selectedRange: NSRange ΧʔιϧͷҐஔΛऔಘ͢Δ 1 OR

Slide 35

Slide 35 text

• swift-markdownͰ͸ൣғΛࣔ͢ͷʹSourceRange͕࢖ΘΕΔ • ΧʔιϧҐஔΛSourceRangeʹม׵͢Δඞཁ͕͋Δ 35 /// A range in a source file. public typealias SourceRange = Range ΧʔιϧҐஔΛswift-markdown͕ѻ͑ΔΑ͏ʹม׵͢Δ 2

Slide 36

Slide 36 text

36 /// A location in a source file. public struct SourceLocation: Hashable, CustomStringConvertible, Comparable { public static func < (lhs: SourceLocation, rhs: SourceLocation) -> Bool { if lhs.line < rhs.line { return true } else if lhs.line == rhs.line { return lhs.column < rhs.column } else { return false } } /// The line number of the location. public var line: Int /// The number of bytes in UTF-8 encoding from the start of the line to the character at this source location. public var column: Int /// The source file for which this location applies, if it came from an accessible location. public var source: URL? /// Create a source location with line, column, and optional source to which the location applies. /// /// - parameter line: The line number of the location, starting with 1. /// - parameter column: The column of the location, starting with 1. /// - parameter source: The URL in which the location resides, or `nil` if there is not a specific /// file or resource that needs to be identified. public init(line: Int, column: Int, source: URL?) { self.line = line self.column = column self.source = source } public var description: String { let path = source.map { $0.path.isEmpty ? "" : "\($0.path):" } ?? "" return "\(path)\(line):\(column)" } } SourceLocationͷ࣮૷

Slide 37

Slide 37 text

37 public struct SourceLocation: Hashable, CustomStringConvertible, Comparable { /// Create a source location with line, column, and optional source to which the location applies. /// /// - parameter line: The line number of the location, starting with 1. /// - parameter column: The column of the location, starting with 1. /// - parameter source: The URL in which the location resides, or `nil` if there is not a specific /// file or resource that needs to be identified. public init(line: Int, column: Int, source: URL?) { self.line = line self.column = column self.source = source } }

Slide 38

Slide 38 text

38 public struct SourceLocation: Hashable, CustomStringConvertible, Comparable { /// Create a source location with line, column, and optional source to which the location applies. /// /// - parameter line: The line number of the location, starting with 1. /// - parameter column: The column of the location, starting with 1. /// - parameter source: The URL in which the location resides, or `nil` if there is not a specific /// file or resource that needs to be identified. public init(line: Int, column: Int, source: URL?) { self.line = line self.column = column self.source = source } }

Slide 39

Slide 39 text

SourceLocationͷ࣮૷ 39 • line: શମͷԿߦ໨͔ • column: ߦ಄͔ΒԿจࣈ໨͔

Slide 40

Slide 40 text

40 public struct SourceLocation: Hashable, CustomStringConvertible, Comparable { /// The line number of the location. public var line: Int /// The number of bytes in UTF-8 encoding from the start of the line to the character at this source location. public var column: Int /// Create a source location with line, column, and optional source to which the location applies. /// /// - parameter line: The line number of the location, starting with 1. /// - parameter column: The column of the location, starting with 1. /// - parameter source: The URL in which the location resides, or `nil` if there is not a specific /// file or resource that needs to be identified. public init(line: Int, column: Int, source: URL?) { self.line = line self.column = column self.source = source } }

Slide 41

Slide 41 text

SourceLocationͷ࣮૷ 41 • line: શମͷnߦ໨ ‣ n͸1࢝·Γ • column: ߦ಄͔Βmจࣈ໨ ‣ m͸1࢝·Γ ‣ UTF-8ͰͷByte਺

Slide 42

Slide 42 text

😁 ΧʔιϧҐஔΛSourceLocationʹม׵͢Ε͹OK 42

Slide 43

Slide 43 text

ΧʔιϧҐஔ 43 @NSCopying var selectedTextRange: UITextRange? { get set } textView.selectedRange.location // 文章全体のn文字目 textView.selectedRange.length // locationからm文字目 textView.selectedTextRange?.start // 文章の先頭からn文字目 textView.selectedTextRange?.end // 文章の先頭からm文字目 open var selectedRange: NSRange

Slide 44

Slide 44 text

😭 ߦͳΜͯͳ͍ 44

Slide 45

Slide 45 text

💪 UITextRange͔ΒSourceLocationʹม׵ 45

Slide 46

Slide 46 text

46 import UIKit import Markdown extension UITextView { func sourceLocation() -> SourceLocation? { guard let selectedTextRange else { return nil } // ドキュメントの先頭からカーソル位置までの文字列を取得 guard let beginningOfDocumentToCursorPositionRange = textRange(from: beginningOfDocument, to: selectedTextRange.start), let beginningOfDocumentToCursorPositionString = text(in: beginningOfDocumentToCursorPositionRange) else { return nil } let lineCount = beginningOfDocumentToCursorPositionString.components(separatedBy: .newlines).count // カーソルがある行の行頭からカーソル位置までの文字列を取得 guard let lineRange = tokenizer.rangeEnclosingPosition(selectedTextRange.start, with: .line, inDirection: .storage(.forward)), let columnStringRange = textRange(from: lineRange.start, to: selectedTextRange.start), let columnString = text(in: columnStringRange) else { return nil } // SourceLocationのcolumnはUTF-8でカウントする // SourceLocationはカウントが1から始まるので1を足す let utf8BasedColumnCount = columnString.utf8.count + 1 return SourceLocation(line: lineCount, column: utf8BasedColumnCount, source: nil) } } ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ ※ϓϩμΫγϣϯͰ࣮૷͢Δ৔߹͸UITextViewΛExtension͠ͳ͍΄͏͕ྑ͍Α UITextRange͔ΒSourceLocationʹม׵͢Δίʔυ

Slide 47

Slide 47 text

MarkupWalkerΛ࢖͏ 47 import Markdown struct StrongWalker: MarkupWalker { var isStrong = false var cursorSourceLocation: SourceLocation mutating func visitStrong(_ strong: Strong) -> () { if let range = strong.range, range.contains(cursorSourceLocation) { isStrong = true return } descendInto(strong) } } Visitor patternʹै࣮ͬͯ૷ ΧʔιϧҐஔ͕ଠࣈ͔Ͳ͏͔֬ೝ͢Δ 3

Slide 48

Slide 48 text

MarkupWalkerΛ࢖͏ 48 import Markdown struct StrongWalker: MarkupWalker { var isStrong = false var cursorSourceLocation: SourceLocation mutating func visitStrong(_ strong: Strong) -> () { if let range = strong.range, range.contains(cursorSourceLocation) { isStrong = true return } descendInto(strong) } } Visitor patternʹै࣮ͬͯ૷ ΧʔιϧҐஔ͕ଠࣈ͔Ͳ͏͔֬ೝ͢Δ 3

Slide 49

Slide 49 text

MarkupWalkerΛ࢖͏ 49 import Markdown struct StrongWalker: MarkupWalker { var isStrong = false var cursorSourceLocation: SourceLocation mutating func visitStrong(_ strong: Strong) -> () { if let range = strong.range, range.contains(cursorSourceLocation) { isStrong = true return } descendInto(strong) } } Visitor patternʹै࣮ͬͯ૷ ΧʔιϧҐஔ͕ଠࣈ͔Ͳ͏͔֬ೝ͢Δ 3

Slide 50

Slide 50 text

50 guard let cursorSourceLocation = textView.sourceLocation() else { return } let document = Document(parsing: textView.text) var strongWalker = StrongWalker(cursorSourceLocation: cursorSourceLocation) strongWalker.visit(document) print(strongWalker.isStrong) // カーソルが太字だとtrue ΧʔιϧҐஔ͕ଠࣈ͔Ͳ͏͔֬ೝ͢Δ 3

Slide 51

Slide 51 text

σϞಈը 51

Slide 52

Slide 52 text

52 • ଠࣈ͡Όͳ͔ͬͨΒ ‣ "****"Λૠೖͯ͋͛͠Δ ‣ ૠೖޙʹΧʔιϧΛҠಈͯ͋͛͠Δͱ͞Βʹ਌੾ • ଠࣈͩͬͨΒ ‣ ͜Ε΋swift-markdownͰରԠ͢Δ ଠࣈه߸Λૠೖ or ࡟আ͢Δ 4

Slide 53

Slide 53 text

ΧʔιϧҐஔͷଠࣈΛղআ 53 struct StrongRemover: MarkupRewriter { var cursorSourceLocation: SourceLocation func visitStrong(_ strong: Strong) -> Markup? { if let range = strong.range, range.contains(cursorSourceLocation) { return Text(strong.plainText) } return strong } } var strongRemover = StrongRemover(cursorSourceLocation: cursorSourceLocation) let result = strongRemover.visit(document) textView.text = result!.format() ※ෳࡶͳMarkdownʹରԠ͢Δʹ͸Text(strong.plainText)Λฦ͢ͷͰ͸ͳ͘ɺஸೡͳॲཧ͕ඞཁͰ͢ MarkupRewriterΛར༻͢Δ

Slide 54

Slide 54 text

54 ೖྗิॿ͕׬੒ 🎉

Slide 55

Slide 55 text

γϯλοΫεϋΠϥΠτΛ࡞Ζ͏ 55

Slide 56

Slide 56 text

γϯλοΫεϋΠϥΠτʢଠࣈͷΈʣ 56 1 MarkupWalkerͰଠࣈه๏ͷSourceRangeΛऔಘ SourceRangeΛNSRangeʹม׵ NSAttributedStringΛੜ੒ 2 3

Slide 57

Slide 57 text

57 struct AttributedStringWalker: MarkupWalker { var strongSourceRanges: [SourceRange] = [] mutating func visitStrong(_ strong: Strong) -> () { if let range = strong.range { strongSourceRanges.append(range) } descendInto(strong) } } 1 MarkupWalkerͰଠࣈه๏ͷSourceRangeΛऔಘ

Slide 58

Slide 58 text

58 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ

Slide 59

Slide 59 text

59 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ

Slide 60

Slide 60 text

60 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ

Slide 61

Slide 61 text

61 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ

Slide 62

Slide 62 text

62 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ

Slide 63

Slide 63 text

63 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ SourceLocation: UTF-8 NSAttributedString: UTF-16 ref: https://developer.apple.com/documentation/foundation/nsattributedstring/1418432-length Χ΢ϯτํ๏ͷҧ͍ʹ஫ҙ

Slide 64

Slide 64 text

64 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ SourceLocation: UTF-8 NSAttributedString: UTF-16 ref: https://developer.apple.com/documentation/foundation/nsattributedstring/1418432-length Χ΢ϯτํ๏ͷҧ͍ʹ஫ҙ

Slide 65

Slide 65 text

65 extension UITextView { func convertToNSRange(from sourceRange: SourceRange) -> NSRange? { guard let start = convertToBound(from: sourceRange.lowerBound), let end = convertToBound(from: sourceRange.upperBound) else { return nil } return NSRange(start.. Int? { var currentLine: Int = 1 var utf16CharCount: Int = 0 for line in text.components(separatedBy: .newlines) { if currentLine == sourceLocation.line { let columnString = line.utf8.prefix(sourceLocation.column - 1) let columnStringUtf16Count = columnString.endIndex.utf16Offset(in: line) return utf16CharCount + columnStringUtf16Count } currentLine += 1 utf16CharCount += line.utf16.count + "\n".utf16.count } return nil } } SourceRangeΛNSRangeʹม׵ 2 ௕͍ͷͰޙ͔Βࢿྉ΍αϯϓϧϦϙδτϦΛݟͯͶ

Slide 66

Slide 66 text

66 let document = Document(parsing: textView.text) var strongWalker = AttributedStringWalker() strongWalker.visit(document) let attributedString = NSMutableAttributedString(string: textView.text) for strongRange in strongWalker.strongSourceRanges { let range = textView.convertToNSRange(from: strongRange)! attributedString.addAttributes([.font: UIFont.systemFont(ofSize: 20, weight: .bold)], range: range) } 
 textView.attributedText = attributedString NSAttributedStringΛੜ੒ 3

Slide 67

Slide 67 text

67 let document = Document(parsing: textView.text) var strongWalker = AttributedStringWalker() strongWalker.visit(document) let attributedString = NSMutableAttributedString(string: textView.text) for strongRange in strongWalker.strongSourceRanges { let range = textView.convertToNSRange(from: strongRange)! attributedString.addAttributes([.font: UIFont.systemFont(ofSize: 20, weight: .bold)], range: range) } 
 textView.attributedText = attributedString NSAttributedStringΛੜ੒ 3

Slide 68

Slide 68 text

68 let document = Document(parsing: textView.text) var strongWalker = AttributedStringWalker() strongWalker.visit(document) let attributedString = NSMutableAttributedString(string: textView.text) for strongRange in strongWalker.strongSourceRanges { let range = textView.convertToNSRange(from: strongRange)! attributedString.addAttributes([.font: UIFont.systemFont(ofSize: 20, weight: .bold)], range: range) } 
 textView.attributedText = attributedString NSAttributedStringΛੜ੒ 3

Slide 69

Slide 69 text

69 let document = Document(parsing: textView.text) var strongWalker = AttributedStringWalker() strongWalker.visit(document) let attributedString = NSMutableAttributedString(string: textView.text) for strongRange in strongWalker.strongSourceRanges { let range = textView.convertToNSRange(from: strongRange)! attributedString.addAttributes([.font: UIFont.systemFont(ofSize: 20, weight: .bold)], range: range) } 
 textView.attributedText = attributedString NSAttributedStringΛੜ੒ 3

Slide 70

Slide 70 text

70 let document = Document(parsing: textView.text) var strongWalker = AttributedStringWalker() strongWalker.visit(document) let attributedString = NSMutableAttributedString(string: textView.text) for strongRange in strongWalker.strongSourceRanges { let range = textView.convertToNSRange(from: strongRange)! attributedString.addAttributes([.font: UIFont.systemFont(ofSize: 20, weight: .bold)], range: range) } 
 textView.attributedText = attributedString NSAttributedStringΛੜ੒ 3

Slide 71

Slide 71 text

71 let document = Document(parsing: textView.text) var strongWalker = AttributedStringWalker() strongWalker.visit(document) let attributedString = NSMutableAttributedString(string: textView.text) for strongRange in strongWalker.strongSourceRanges { let range = textView.convertToNSRange(from: strongRange)! attributedString.addAttributes([.font: UIFont.systemFont(ofSize: 20, weight: .bold)], range: range) } 
 textView.attributedText = attributedString NSAttributedStringΛੜ੒ 3

Slide 72

Slide 72 text

γϯλοΫεϋΠϥΠτ׬੒ʂ 72

Slide 73

Slide 73 text

ೖྗิॿͱ γϯλοΫεϋΠϥΠτΛ ଞͷه๏ʹରͯ͠΋࣮૷͢Ε͹׬੒ 73

Slide 74

Slide 74 text

74 ΋ͬͱஸೡʹγϯλοΫεϋΠϥΠτΛ࡞Δ https://fortee.jp/iosdc-japan-2020/proposal/32f815cc-8b16-4321-9cf4-c74f70287190

Slide 75

Slide 75 text

Preview͸Ͳ͏ͨ͠Βʁ 75

Slide 76

Slide 76 text

Preview 76 NSAttributedStringͱSwiftUIΛࢼ͢

Slide 77

Slide 77 text

77 SwiftUI

Slide 78

Slide 78 text

78 NSAttributedString

Slide 79

Slide 79 text

Ͳ͏͢ΔͱΑ͍ʁ 79 • swift-markdownͰHTMLʹม׵ͯ͠WebViewͰදࣔ • keitaoouchi/MarkdownView ͳͲͷϥΠϒϥϦΛར༻͢Δ • markedjs/marked ΛಡΈࠐΜͩHTMLΛWebViewͰදࣔ͢Δ

Slide 80

Slide 80 text

Ͳ͏͢ΔͱΑ͍ʁ 80 • swift-markdownͰHTMLʹม׵ͯ͠WebViewͰදࣔ • keitaoouchi/MarkdownView ͳͲͷϥΠϒϥϦΛར༻͢Δ • markedjs/marked ΛಡΈࠐΜͩHTMLΛWebViewͰදࣔ͢Δ ΞϓϦͷ࢓༷ʹ߹Θͤͯબ΅͏

Slide 81

Slide 81 text

·ͱΊ 81 • Markdownͷೖྗิॿ͸ਖ਼نදݱͰ͸೉͍͠ • ؤுͬͯSourceLocationΛม׵͠Α͏ • MarkupWalker, MarkupRemoverΛ࢖͑͹͍͍ͩͨͳΜͰ΋Ͱ͖Δ

Slide 82

Slide 82 text

͕࣌ؒ༨ͬͨ࣌ͷTips 82

Slide 83

Slide 83 text

͜ͷೖྗิॿόʔΛ ΩʔϘʔυʹ௥ैͤ͞Δํ๏ 83 iOS15+

Slide 84

Slide 84 text

StoryboardͰઃఆ͢Δํ๏ 84

Slide 85

Slide 85 text

StoryboardͰઃఆ͢Δํ๏ 85

Slide 86

Slide 86 text

StoryboardͰઃఆ͢Δํ๏ 86

Slide 87

Slide 87 text

StoryboardͰઃఆ͢Δํ๏ 87

Slide 88

Slide 88 text

ίʔυ্Ͱઃఆ͢Δํ๏ 88 NSLayoutConstraint.activate([ view.keyboardLayoutGuide.topAnchor.constraint(equalTo: バーのView.bottomAnchor, constant: 0) ])

Slide 89

Slide 89 text

ίʔυ্Ͱઃఆ͢Δํ๏ 89 NSLayoutConstraint.activate([ view.keyboardLayoutGuide.topAnchor.constraint(equalTo: バーのView.bottomAnchor, constant: 0) ])