• Swift Package는 원래 소스코드 자체를 배포할 수 있는 상황에서만 사용 가능했다.

    • 소스코드를 배포할 수 있는 상황이 항상 있는 것은 아니기 때문에, 새로 도입된 xcframework를 통해서 바이너리 형태로 배포도 가능하다.
  • XCFramework?

    • xcode가 지원하는 모든 플랫폼과 그 변종(시뮬레이터)에 대한 프레임워크를 담을 수 있다.
      • 프레임워크 뿐 아니라 static library와 헤더도 담을 수 있다.
    • 사용하는 것은 .framework와 동일하다.
  • 사용시 고려사항

    • 프레임워크는 앱의 권한을 그대로 따라간다.
      • 프레임워크의 코드를 쓰기 위해서는 코드를 신뢰할 수 밖에 없다.
      • 프레임워크가 특정 권한이 있어야 제대로 동작하는 경우에도, 그걸 챙기는 건 앱의 역할이다.
  • XCFramework 만들기

    • Build Libraries for distribution 키기

      • 이 옵션이 없던 과거에 만날 수 있었던 오류: Compiled module was created by a newer version of the compiler
        • swift에서 import를 할 경우, 해당 모듈의 Compiled Module Format(.swiftmodule) 파일을 읽고, 여기에서 public 인터페이스를 검색해서 원하는 심볼을 쓸 수 있다.
        • Compiled Module Format은 Serialized된 binary 형태
        • 컴파일러 내부적인 데이터 구조를 쓰고, 이는 컴파일러 버전에 따라 달라진다.
      • 그래서 xcode 11부터는 Swift Module Interface라는 것을 도입
        • 퍼블릭 API를 텍스트 형태로 리스팅한 것
        • 컴파일러 버전에 상관 없이 호환
      • 구조
        • 원본

          import UIKit
          
          public class Spaceship {
          	public let name: String
          	private var currentLocation: Location
          	
          	public init(name: String) {
          		self.name = name
          		currentLocation = .launchpad
          	}
          
          	public func fly(
          		to destination: Location,
          		speed: Speed) {
          		currentLocation = destination
          	}
          }
          
          public enum Speed {
          	case leisurely
          	case fast
          }
          
          public struct Location {
          	public var coordinated: Coordinates
          }
          
          
        • swift module interface

          • public으로 명시한 것만 있다.
          • 시그니처만 있고, 본문은 없다. → 인터페이스만 있기 때문에 버전간 호환성을 유지할 수 있다.
          • deinit은 없을 경우 자동으로 만들어준다.
          // swift-interface-format-version: 1.0
          // swift-compiler-version: Swift version 5.1
          // swift-module-flags: -target arm64-apple-ios13.0 -enable-library-evolution -swift-version 5 -O -module-name FlightKit
          
          import Swift
          import UIKit
          
          public class Spaceship {
          	public let name: Swift.String
          	
          	public init(name: Swift.String)
          	
          	public func fly(
          		to destination: FlightKit.Location,
          		speed: FlightKit.Speed)
          
          	@objc deinit
          }
          
          // 연관값이 없을 경우 Hashable이 자동으로 추가됨
          public enum Speed: Swift.Hashable {
          	case leisurely
          	case fast
          	
          	public static func ==(
          		a: FlightKit.Speed,
          		b: FlightKit.Speed) -> Swift.Bool 
          	public func hash(into hasher: inout Swift.Hasher)
          }
          
          public struct Location {
          	public var coordinated: FlightKit.Coordinates
          }
          
    • 아카이브 하기

      • 릴리즈 모드로 빌드한다.

      • Xcode Organizer에서 찾을 수 있다.

      • dSYM 정보를 가지고 있게 된다.

      • CLI로 빌드하기

        xcodebuild archive \\
        	-scheme FlightKit \\
        	-destination "..." \\
        	-destination "..." \\
        	...
        	SKIP_INSTALL=NO \\  # 이게 있어야 아카이브 안에 framework가 존재한다.
        
    • xcframework로 엮어내기

      xcodebuild -create-xcframework \\
      	-framework [path] \\
      	-framework [path] \\
      		...
      	-output FlightKit.xcframework
      
  • 프레임워크 작성시 고려사항

    • Evolving your Framework: Semantic Versioning을 유념할 것
      • 인터페이스는 최대한 작게: 추가는 쉽지만, 빼는 것은 어렵다.
      • 이름과 요구사항은 최대한 신중하게
      • 불필요한 확장성은 최대한 피해야 한다.(open 클래스, 임의의 콜백 등)
    • Trading flexibility for optimization
      • 프레임워크의 동작 원리: indirection
        • 프레임워크의 함수 주소, 자료형 크기 등은 모두 프레임워크가 런타임에 확인한다.
          • Objective-C에서의 message dispatch와 근본은 같지만, Swift는 이게 client -framework 경계선에서만 일어난다.
      • 프레임워크 입장에서는 최대한 유연하게 만들어서 호환이 되게 만드는 게 좋지만, 클라이언트는 프레임워크 안에서 뭐가 일어나는 지 알아야 최적화를 할 수 있다.
        • build libraries for distribution을 키는 것은 flexibility를 높이는 것이다.
        • @inlinable, @frozen 등으로 유연성을 희생해서 성능을 높일 수 있다.
          • inlinable: body까지도 interface의 일부로 추가하는 것
            • @usableFromInline 어노테이션으로 internal 프로퍼티도 interface의 일부로 흡수하면서도 internal 속성을 유지할 수 있다.
            • 다만 inline된 상태에서 함수의 output을 바꾼다던지 하면, 재컴파일 하기전까지 잘못된 로직이 박혀있는 상태가 된다.
          • frozen: 해당 타입의 레이아웃이 바뀌지 않을 것임을 보장
            • enum인 경우는 새로운 case가 추가되거나 연관값이 변하지 않아야 한다.
            • struct인 경우는 프로퍼티의 추가, 삭제, 재배열이 없어야 하고 모든 저장 프로퍼티가 public혹은 @usableFromInline이여야함
              • inlinable한 생성자 사용 가능
      • 기본 동작은 flexible
        • 수정할 때마다 breaking Change를 만드는 것은 불편하니까.
        • 이 attribute는 클라이언트 코드에만 영향을 준다.
        • 반드시 Profile을 한 뒤에 결정하자.
    • Helping your clients
      • 특정 권한이 필요하면 명문화해서 사용자화해서 알려줘야 한다.
        • 이런 권한은 최소한으로 요구해야 한다.
        • 거절하더라도 우아하게 풀어내야 한다.
      • 의존성을 명문화하고, 추가 의존성은 최소화해야한다.
        • 모두 build libraries for distribution을 키는 것이 좋다.
      • 바이너리 프레임워크는 다른 패키지에 의존할 수 없다.
      • Swift에 Objective-C API가 없다면, Objective-C 호환 헤더 옵션을 끄라
        • Define Module 옵션도 끄라. 이러면 umbrella header가 없어진다.