• AttributedString

    • Characters + Ranges + Dictionary

    • 기존 NSAttributedString을 대체하는 AttributedString 타입 추가

      • 값 타입
      • String과 호환
      • Localizable
      • type-safe하고, secure한 Codable도 지원
      var thanks = AttributedString("Thank You!")
      thanks.font = .body.bold()
      
      var website = AttributedString("Please visit our website.")
      website.font = .body.italic()
      website.link = URL(string: "<http://www.example.com>")
      
      var container = AttributeContainer(
      if important {
      	container.foregroundColor = .red
      	container.underlineColor = .primary
      } else {
      	container.foregroundColor = .primary
      }
      
      thanks.mergeAttributes(container)
      website.mergeAttributes(container)
      
      • AttributedString 자체는 콜렉션이 아니지만, 콜렉션으로 다룰 수 있는 View를 제공한다.
        • 글자별로 다루기위해서는 characters
        • attribute별로 다루기 위해서는 runs
      // characters
      let characterView = message.characters
      for i in characterView.indices where characterView[i].isPunctuation {
      	message[i..<characterView.index(after: i)].foregroundColor = .orange
      }
      
      // runs
      let runCount = message.runs.count
      
      let firstRun = message.runs.first!
      let firstString = String(message.characters[firstRun.range])
      
      // 특정 Attribute 기준으로만 다루기
      let linkRunCount = message.runs[\\.link].count
      
      var insecureLinks: [URL] = []
      for (value, range) in message.runs[\\.link] {
      	if let v = value, v.scheme != "https" {
      		insecureLinks.append(v)
      	}
      }
      
      // mutation
      
      if let range = message.range(of: "visit") {
      	message[range].font = .body.italic().bold()
      	message.characters.replaceSubrange(range, with: "surf")
      } 
      
      • localization → 이제 포맷 문자열 필요없다.
        • Xcode Option에서 이를 자동으로 추출해주는 기능이 생겼다.
      func prompt(for document: String) -> String {
      	String(localized: "Would you like to save the document \\"\\(document)\\"?")
      }
      
      func attributedPrompt(for document: String) -> AttributedString {
      	AttributedString(localized: "Would you like to save the document \\"\\(document)\\"?")
      }
      
      • 마크다운을 네이티브로 지원한다
        • SwiftUI의 Text 속성에서는 그냥 넣으면 된다.
      • Conversion
        • NSAttributedString - 생성자에 그냥 넣으면 바꿔준다.

        • Coding - 기본적으로 Codable을 채택했기 때문에 신경쓸 것은 없다.

        • 커스텀 Attribute

          • Key
            • AttributedStringKey 프로토콜을 채택하고 있다.
            • Value의 타입을 지정한다.
            • Value가 어떻게 인코딩 되고 디코딩 되는지를 커스텀할 수 있다.
          enum RainbowAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey  { // Codable이 필요한 경우
          	enum Value: String, Codable { // associatedType
          		case plain
          		case fun
          		case extreme
          	}
          
          	public static var name = "rainbow"
          }
          
          • 커스텀 마크다운 Attribute
            • 다른 API에도 JSON5를 지원하도록 변경됨
          This text contains [a link](<http://www.example.com>)
          
          This text contains ![an image](<http://www.example.com/my_image.gif>).
          
          // 여기까지는 표준 마크다운
          
          //  Custom Attribute, JSON 5를 기준으로 만들어짐
          This text contains ^[an attribute](rainbow: 'extreme')
          
          This text contains ^[two attributes](rainbow: 'extreme', otherValue: 42).
          
          This text contains ^[an attribute with 2 properties](someStuff: { key: true, key2: false}).
          
          • Attribute Scopes
            • AttributeKey의 그룹. 즉 어떤 키를 만났을 때 이를 어떻게 해석할지를 써놓은 것이라 이해하면 된다.
            • 커스텀 마크다운을 파싱할 때 필요하다. 그 외에도 Scope를 받는 것들이 있다.
            • 앱단위, 프레임워크 단위로 하나씩 만드면 된다.
          extension AttributeScopes {
          	struct CaffeAppAttributes: AttributeScope {
          		let rainbow: RainbowAttribute
          		
          		let swiftUI: SwiftUIAttributes // 시스템 스코프를 포함한다.
          	}
          
          	var caffeApp: CaffeAppAttributes.Type { CaffeAppAttributes.self } 
          }
          
          let header = AttributedString(localized: "^[Fast & Delicious](rainbow: 'extreme') Food",
          															including: \\.caffeApp)
          
          
  • Formatters

    • 포매터를 만들고 성능을 위해 캐시하고 하는게 영 복잡하다.
    • 그래서 API를 다시 만들었다. 포매터에 데이터를 넣는 게 아니라 데이터의 포매팅 메소드를 호출하도록.
    • 포맷 스트링도 쉽게 쓸 수 있도록 만들었다. 컴파일 타임 안정성을 챙길 수 있도록
    • 예제
      • Date Formatting

        • Date(그냥 숫자값)에 Calendar, TimeZone을 이용해서 사람이 읽기 편한 형태로 바꿔주는것
        • 크게 dateTime과 iso8601 방식으로 나눌 수 있고, 여기서 필요한 필드를 지정하는 방식
        • 순서는 상관없다.
        • 필드를 지정할 때, 그럴듯한 기본값을 제공해서 쉽게 호출할 수 있다.
        • 커스터마이징도 가능하다.
        let date = Date.now
        
        let formatted = date.formatted() // formatted(.dateTime)과 동일
        // "6/7/2021, 9:42AM"
        
        let onlyDate = date.formatted(date: .numeric, time: .omitted)
        // onlyDate is "6/7/2021"
        
        let onlyTime = date.formatted(date: .omitted. time: .shortened)
        // onlyTime is "9:42 AM"
        
        let formatted = date.formatted(.dateTime.year().day().month())
        // "Jun 7, 2021"
        
        date.formatted(.dateTime.year().day().month(.wide))
        // "June 7, 2021"
        
        date.formatted(.dateTime.weekday(.wide))
        // "Monday"
        
        date.formatted(.iso8601)
        // "20210607T164200Z"
        
        date.formatted(.iso8601.year().month().day().dateSeparator(.dash))
        // "2021-06-07"
        
        • 심지어 range에 대해서도 가능하다.
        let now = Date.now
        let later = new + TimeInterval(5000)
        
        let range = (now..<later).formatted()
        "6/7/21, 9:42 - 11:05AM"
        
        let noDate = (now..<later).formatted(date: .omitted, time: .complete)
        "9:42:00 AM PDT - 11:05:20 AM PDT"
        
        let timeDuration = (now..<later).formatted(.timeDuration)
        "1:23:20"
        
        let components = (now..<later).formatted(.components(style: .wide))
        "1 hour, 23 minutes, 20 seconds"
        
        let relative = later.formatted(.relative(presentation: .named, unitsStyle: .wide))
        "in 1 hour"
        
        • Attribute 적용도 간편하다.
        var str = date.formatted(.dateTime
        														.minute()
        														.hour()
        														.weekday()
        														.locale(locale)
        														.attributed)
        let weekday = AttributeContainer.dateField(.weekday)
        
        let color = AttributedContainer
        						.foregroundColor(.orange)
        
        str.replaceAttributes(weekday, with: color)
        
        • 반대로 String을 Date로 바꾼다면?
        let format = Date.FormatStyle().year().day().month()
        let formatted = date.formatted(format)
        // "Jun 7, 2021"
        
        if let date = try? Date(formatted, strategy: format) {
        	// 2021-06-07 07:00:00 +0000
        }
        
        let strategy = Date.ParseStrategy(
        	format: "\\(year: .defaultDigits)-\\(month: .twoDigits)-\\(day: .twoDigits)",
        	timeZone: TimeZone.current)
        
        if let date = Date("2021-06-07", strategy: strategy) {
        	// 2021-06-07 07:00:00 +0000
        }
        
      • Number formatting

        12345.formatted()
        //"12,345"
        
        25.formatted(.percent) 
        // 25%
        
        42e9.formatted(.number.notation(.scientific))
        // 4.2E10
        
        29.formatted(.currency(code: "usd"))
        // $29.00
        
        let list = [25, 50, 75].formatted(.list(memberStyle: .percent, type: .or))
        // "25%, 50%, or 75%"
        
  • Grammar agreement

    • 언어에 따라 문법적으로 실수할 여지가 많다.(단수복수 문제, 남성형, 여성형 단어 차이 등)
    • 이러한 것들을 제대로 쓰는 것을 agreement라고 한다.
    • 이를 지원하기 위해서 Automatic Grammer Agreement가 추가 된다. → 영어, 스페인어
      • inflection이라는 것을 통해서 지원된다.