ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • What's new in SwiftUI (feat. WWDC 2023)
    SwiftUI 2023. 6. 7. 12:14

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 WWDC 2023에서 소개된 SwiftUI의 새로운 기능 및 개선된 사항들을 학습해보겠습니다🙋🏻

     

    들어가기 앞서

    작년 WWDC 2022에서 SwiftUI4가 소개되면서 정말 많은 발전이 있었죠!?

    차트나 네비게이션 방식의 전체적인 변화 등 실제적으로 체감되는 변화들이 많았었습니다.

    그런데 이번 SwiftUI의 소개를 보면 작년처럼 많은 변화와 체감되는 업데이트 항목들이 많았습니다😃

    좀 더 편리하게 SwiftUI를 사용할 수 있고 오히려 처음 접하는 개발자들에게는 러닝커브도 조금 준듯 해보였죠.

    점점 더 진화하는 SwiftUI를 보면서 편리하기도 하면서 위기감도 들더라구요ㅎㅎ

     

    그럼 한번 어떤것들이 이번에는 소개되고 변화되었는지 바로 알아보시죠🕺🏻

     

    SwiftUI in more places

    이젠 새로운 플랫폼을 포함해 더 많은 곳에서 SwiftUI를 사용할 수 있게 되었습니다.

    헤드셋 및 wathOS 10에서 새로운 위젯 및 프레임워크 간 통합에 이르기까지 SwiftUI를 사용하여 구성할 수 있습니다.

    특히 visionOS도 한부분을 차지하니 놀랍습니다.

    공간 컴퓨팅에서의 새로운 3D 기능으로 SwiftUI를 새로운 미래로 가져다 주게 됩니다.

    또한 이러한 구현을 위해 새로운 3D 제스쳐, 효과 및 레이아웃 등 RealityKit과의 통합을 해줍니다.

    정말 이제 모든것들을 SwiftUI가 관여하네요.

     

    이제 새로운 공간 컴퓨팅이라는 플랫폼에서 WindowGroup과 같은 이미 친숙한 유형을 사용해 창을 구성합니다.

    WindowGroup scene은 깊이 감지 3D 컨트롤을 사용해 2D window로 렌더링됩니다.

     

    window 내에서 이렇게 기본적으로 SwiftUI에서 사용하는 NavigationSplitView나 TabView를 이용할 수 있죠.

    이처럼 다른 플랫폼과 마찬가지로 모든 일반적인 SwiftUI 컨트롤을 사용할 수 있습니다.

     

    아주 간단히 깊이를 더하여 표현하고 싶으면 windowStyle을 지정해주면 됩니다.

    볼륨은 제한된 공간에서 게임이나 건축 모델과 같은 3D 경험을 표시합니다.

     

    실제로 RealityKit을 이용해 정적 모델 혹은 동적인 대화명 모델로 볼륨을 채울 수 있죠.

     

    만약 완전히 몰입하여 채우려면 ImmersiveSpace를 사용할 수 있습니다.

    요런 공간 컴퓨팅에 대해 더 알아보려면 Meet SwiftUI for spatial computing 섹션을 보면 좋을것 같네요🙋🏻

     

    중요한것은 이 모든것이!? SwiftUI로 할 수 있다라는 겁니다.

     

    더 나아가 wathOS 10에서도 SwiftUI를 활용해 집중된 컨텐츠와 정보를 잘 전달하여 더 나은 사용자 경험을 제공할 수 있습니다.

    특히 디자인적으로 더 발전되었는데 이러한 근간에는 wathOS 10용으로 새로 강화된 몇가지 기존 SwiftUI 뷰가 사용되었습니다.

    특히 새로운 containerBackground 모디파이어를 이용해 컨텐츠를 푸시/팝할때 애니메이션을 적용해 더 세밀한 배경 변화를 줄 수 있어요.

     

    또, 기존 Toolbar를 사용한다면 ToolbarItem에서 포지셔닝 자체를 좀 더 명확하고 세밀하게 배치할 수 있습니다.

    이런 새로운 사항들 외에도 DatePicker 및 리스트를 포함해 일부 기존 API를 이번부터 처음으로 watchOS에 제공하게 개선되었습니다.

     

    그 외에도 iPadOS의 위젯 및 잠금 홈화면 더 나아가 macOS Sonoma에서의 데스크탑 위젯 등 애니메이션이 들어가거나 뷰를 구성할때도 SwiftUI가 이용되었고 그 중 하나가 대화형 애니메이션 위젯에서 사용됩니다.

     

    이제 진짜 SwiftUI가 안쓰이는곳이 없어보이네요?

     

    강력해진 Preview

    또 이걸 말 안할수가 없는데요😄

    Swift 5.9에서 제공되는 매크로 기능을 활용해 SwiftUI에서 #Preview로 미리보기에 진화를 이뤘습니다.

    더 편리하고 깔끔하게 제공해주죠.

    특히 위젯 상태 및 애니메이션을 더 세밀하게 보기 위해 프리뷰의 timeline을 제공해 순차적으로 확인할 수 있습니다.

     

    또, Xcode 내에서 바로 Mac 앱의 미리보기를 할 수 있습니다.

    프리뷰에 대해 조금 더 자세히 확인하려면 Build programmatic UI with Xcode Previews 섹션을 보면 되겠네요😲

     

    MapKit

    애플의 매핑 프레임워크 기능을 제공하여 SwiftUI내에서 MapKit의 대규모 업데이트가 이뤄졌습니다.

    사용을 위해서는 간단하게 MapKit과 SwiftUI만 import 하면 됩니다.

     

    Chart

    차트에 대해서도 더욱 새로운 기능들이 많이 생겨났습니다.

     

    StoreKit

    맞춤형 마케팅 콘텐츠로 구독 뷰를 제공합니다.

    인앱 마케팅을 강화할 수 있죠.

     

    이렇듯 새로운 플랫폼과 위젯에서 프레임워크 간 통합 및 watchOS의 아름다움에 이르기까지 SwiftUI가 이제 정말 많은 영향을 끼치고 있고 애플 개발자 경험을 발전시키고 있네요😳

     

    자 이렇게 다양한 플랫폼에 자리잡은 SwiftUI를 알아봤으니 조금 더 단순해진 data flow를 살펴볼까요?

     

    Simplified data flow

    새로운 데이터 흐름 유형은 도메인 모델링을 획기적으로 단순화하여 이전보다 더 쉽고 많은 기능을 제공해주게 되었어요ㅎㅎ

    특히 Inspector와 테이블 개선 사항은 데이터를 표시하는 훌륭한 방법을 제공합니다.

    모든 앱은 데이터 모델로부터 시작하기에 SwiftUI가 앱 데이터 작업을 위해 더 단순화되고 개선된 기능을 선보였어요.

     

    @Observable

    매크로로 만들어진 이 Observable은 SwiftUI에서 모델 유형을 정의하는 새로운 방법이 되었습니다.

    이를 이용해 데이터 흐름에 익숙한 SwiftUI 패턴을 사용하는 동시에 코드를 더 간결하고 효율적으로 구성할 수 있습니다.

    Observation을 보면 아래와 같이 정의되어 있습니다.

    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
    public protocol Observable {
    }
    
    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
    @attached(member, names: named(_$observationRegistrar), named(access), named(withMutation), arbitrary) @attached(memberAttribute) @attached(conformance) public macro Observable() -> () = #externalMacro(module: "ObservationMacros", type: "ObservableMacro")
    
    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
    @attached(accessor) public macro ObservationIgnored() -> () = #externalMacro(module: "ObservationMacros", type: "ObservationIgnoredMacro")
    
    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
    public struct ObservationRegistrar : Sendable {
    
        public init()
    
        public func access<Subject, Member>(_ subject: Subject, keyPath: KeyPath<Subject, Member>) where Subject : Observable
    
        public func willSet<Subject, Member>(_ subject: Subject, keyPath: KeyPath<Subject, Member>) where Subject : Observable
    
        public func didSet<Subject, Member>(_ subject: Subject, keyPath: KeyPath<Subject, Member>) where Subject : Observable
    
        public func withMutation<Subject, Member, T>(of subject: Subject, keyPath: KeyPath<Subject, Member>, _ mutation: () throws -> T) rethrows -> T where Subject : Observable
    }
    
    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
    @attached(accessor) public macro ObservationTracked() -> () = #externalMacro(module: "ObservationMacros", type: "ObservationTrackedMacro")
    
    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
    public func withObservationTracking<T>(_ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void) -> T

    Observation을 import하고 아래처럼 Observable 모델을 구성할 수 있죠.

    @Observable
    class Person: Identifiable {
      var id = UUID()
      var name = "Green"
      var age = 10
    }

    단순히 @Observable을 붙여주면 됩니다.

    더이상 ObservableObject와 달리 Published 즉, 속성 게시자를 붙여주지 않아도 되죠.

     

    이렇게 되면 아래와 같이 실제 사용할 뷰에서 해당 모델 타입의 프로퍼티를 받으면 SwiftUI는 읽은 프로퍼티에 대해 종속성을 자동으로 설정합니다.

    struct PersonCard: View {
      var person: Person
      
      var body: some View {
        Text(person.name)
          .background(.thinMaterial)
      }
    }

    보시면 @StateObject나 @ObservedObject와 같은 프로퍼티 래퍼를 사용할 필요가 없어져 뷰가 더욱 깔끔해집니다.

    읽어온 속성에 대해서만 발생하기에 불필요한 업데이트를 트리거하지 않고 중간 뷰를 통해 모델을 전달할 수 있죠.

     

    물론 아래와 같은 코드에서 @State를 이용해 해당 속성에 대해 바인딩을 하고 전달하는것은 동일하게 사용할 수 있습니다.

    struct ContentView: View {
      var people: [Person] = [
        .init(name: "Green", age: "10"),
        .init(name: "Red", age: "20")
      ]
      
      var body: some View {
        VStack {
          VStack {
            ForEach(people, id: \.id) { person in
              VStack {
                PersonCardView(person: person)
              }
            }
          }
          
          Button(
            action: { people.first?.name = "Orange" },
            label: {
              Text("Change Name")
            }
          )
        }
      }
    }
    
    // MARK: - 사람 카드 뷰
    struct PersonCardView: View {
      @Bindable var person: Person
      
      var body: some View {
        HStack(spacing: 10) {
          Text(person.name)
          
          TextField("Age", text: $person.age)
        }
      }
    }
    
    // MARK: - 사람 모델
    @Observable
    class Person: Identifiable {
      var id = UUID()
      var name = "Green"
      var age = "10"
      
      init(id: UUID = UUID(), name: String, age: String) {
        self.id = id
        self.name = name
        self.age = age
      }
    }

     

    또한 앱 전체의 뷰에서 environment로 전역적으로 가져올 수도 있죠.

    @main
    struct SwiftUI5App: App {
      @State var people: Person?
      
      var body: some Scene {
        WindowGroup {
          ContentView()
            .environment(people)
        }
      }
    }
    
    // MARK: 사용할 뷰
    struct PersonView: View {
      @Environment(Person.self) private var person: Person?
      
      var body: some View {
        ...
      }
    }

    environment 키나 혹은 사용자 지정 키도 지원됩니다.

    핵심은 @Observable로 더 강력하고 간결한 코드를 작성할 수 있다고 보입니다😁

     

    이것들에 대해서는 Discover Observation in SwiftUI 섹션을 더 참고해봐야 할것 같아요!

     

    이제 그럼 이러한 데이터 모델에 대해 모든 변경 사항이 지속되고 매번 추적하는 일이 없도록 하려면 기존에는 코어 데이터를 이용했습니다.
    그런데 이번부터 SwiftData가 등장했어요!

     

     

    SwiftData

    SwiftData가 이번 WWDC 2023에서 주목할만하게 새로 나왔습니다.

    데이터 모델링 및 관리를 위한 완전히 새로운 프레임워크입니다.

    빠르고 확장 가능하며 무엇보다 SwiftUI와 잘 작동한다고 합니다.

    완전하게 코드로 모두 표현됨으로 SwiftUI 앱에 잘 맞겠구요😲

     

    아까 위에서 모델을 옵저빙하기 위해 @Observable을 사용했던것 기억하시나요?

    그럼 이번에는 해당 모델 타입을 SwiftData에 맞게 설정하기 위해 아래와 같이 Model 매크로로 전환해야 합니다.

    import SwiftData
    
    @Model
    class Person: Identifiable {
      var id = UUID()
      var name
      var age
      
      init(id: UUID = UUID(), name: String, age: Int) {
        self.id = id
        self.name = name
        self.age = age
      }
    }

    이걸로 끝!

    SwiftData는 지속성 외에도 모델의 Observable 사용에서의 모든 이점을 동일하게 가집니다.

    왜냐하면 SwiftData 내에 Observation을 가지고 있습니다.

     

    자 그럼 사용해볼까요?

     

    우선 전역적으로 사용하기 위해 App에서 modelContainer로 어떤 모델을 사용할지 지정해줍니다.

    @main
    struct SwiftUI5App: App {
      var body: some Scene {
        WindowGroup {
          ContentView()
        }
        .modelContainer(for: Person.self)
      }
    }

     

    그 다음으로 실제 사용될 뷰로 갑니다.

    struct ContentView: View {
      @Environment(\.modelContext) private var context
      @Query(sort: \.age) var people: [Person]
      
      var body: some View {
        VStack {
          VStack {
            ForEach(people, id: \.id) { person in
              PersonCardView(person: person)
            }
          }
          
          Button(
            action: {
              context.insert(
                Person(
                  name: "Green",
                  age: Int.random(in: 0...100)
                )
              )
              do {
                try context.save()
              } catch {
                print(error.localizedDescription)
              }
            },
            label: {
              Text("Add Person")
            }
          )
        }
      }
    }

    보시면 @Environment로 해당 모델에 대한 modelContext 프로퍼티를 만듭니다.

    그 다음으로 @Query를 이용해 Person 로컬 데이터들이 담겨 사용될 프로퍼티를 만들어줘요.

    쉽게 쿼리문을 코드로 구현한다 생각하면 편리하고 조건을 더 담아서 원하는 쿼리를 자유롭게 만들 수도 있습니다.

    여기서는 저는 age 나이에 따라 정렬될 수 있도록 sort해줬습니다.

    마지막으로 저는 버튼을 누르면 이름은 Green 나이는 랜덤한 모델을 context에 저장하도록 해줬습니다.

     

    그럼 어떻게 작동되는지 볼까요?

     

    이렇게나 쉽게 로컬 데이터를 다룰 수 있네요!

    또한 DocumentGroup을 통해 각 문서의 기본 저장소로 SwiftData를 사용하고 자동으로 모델 컨테이너를 설정할 수 있다고 합니다.

    DocumentGroup을 통해 앱을 실행 시 자동 공유 및 문서 이름 변경 지원, 도구 모음의 실행 취소 컨트롤과 같은 다양한 기능을 얻을 수 있다네요.

     

    좀 더 자세하게는 Meet SwiftData 섹션을 살펴보면 더 자세히 알 수 있겠어요😃

     

    Inspector

    현재 선택되거나 컨텍스트에 대한 세부 정보를 표시하기 위한 새로운 모디파이어입니다.

    인터페이스에서 아래와 같이 별도의 사이드바 섹션으로 표기되는데 macOS말고 iOS에서 적용하게되면 sheet로 뜨더라구요.

    iOS와 같은 작은 사이즈에서는 시트로 표현된다고 합니다.

     

    FileExporter

    새로운 기능 중 파일 및 이미지 내보내기에 대한 대화 상자를 구성하고 이에 대해 레이블 및 버튼을 조정할 수 있습니다.

    보통 위 Inspector를 에디터로 구성하면서 같이 녹이는 느낌입니다.

     

    List & Tables

    해당 항목들은 대부분의 앱을 구성하는 정말 중요한 요소입니다.

    이번 업데이트로 테이블의 열 순서 및 가시성의 사용자 정의를 지원합니다.

    SceneStorage 동적 프로퍼티와 결합해 기본 설정을 앱 실행간 유지할 수 있게됩니다.

    사용자 지정 상태를 나타내는 값을 테이블에 제공하고 각 열에 고유한 식별자를 제공해 사용할 수 있습니다.

    테이블을 통해 좀 더 그룹화가 편리해졌네요.

     

    또한 새로운 DisclosureTableRow를 사용해 그룹화된 테이블 뷰를 구성할 수 있습니다.

    이와 더불어 섹션도 더 좋은 기능들을 제공해줍니다.

     

    자 그럼 이렇게 앱의 뼈대를 구성하기 위해 많은 기능들이 추가되었다면 이제 애니메이션으로 넘어가볼까요?

     

    Extraordinary animations

    어렵고 아직 부족했던 애니메이션 API를 강화해 앱을 사용하는 유저들에게 더 아름다운 경험을 만들어주게 되었습니다.

     

    KeyframeAnimator

    키 프레임 애니메이터를 이용해 여러 속성을 병렬로 애니메이션화 할 수 있습니다.

    첫번째 클로저에서는 values를 받아 로고의 수직 오프셋과 같은 애니메이션 가능한 속성으로 뷰를 만듭니다.

    두번째 클로저에서는 이러한 속성이 시간이 지나면서 어떻게 변경되는지를 병렬적으로 정의합니다.

     

    이에 대해 좀 더 자세히는 Wind your way through advanced animations in SwiftUI 섹션을 참고해봐야겠어요!

     

    phase animator

    병렬적이 아닌 phase animator는 키프레임 애니메이터보다 더 간단하고 순차적입니다.

    즉, 단일 단계 시퀀스를 거치죠.

    이전 애니메이터가 끝나고 이후 애니메이터가 진행됩니다.

    sightingCount가 변경될때 마다 정의한 단계적인 애니메이터를 실행하도록 합니다.

    우선 첫번째 클로저에서 현재 단계를 기반으로 회전 및 배율을 설정하죠.

    그 다음 두번째 클로저에서 각 단계마다 해당하는 애니메이션을 줍니다.

     

    Custom Animation

    커스텀한 애니메이션도 쉽게 만들 수 있습니다.

    struct ContentView: View {
      @State private var transitionBtnClicked: Bool = false
      
      var body: some View {
        // 애니메이션
        VStack {
          if transitionBtnClicked {
            Rectangle()
              .fill(.green)
              .frame(width: 100, height: 100)
          }
          
          Button("Let's Animation") {
            withAnimation(.init(GreenAnimation(duration: 2.0))) {
              transitionBtnClicked.toggle()
            }
          }
        }
      }
    }
    
    struct GreenAnimation: CustomAnimation {
      var duration: CGFloat
      
      func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
        if time > duration { return nil }
        
        return value.scaled(by: time / duration)
      }
    }

    CustomAnimation 프로토콜을 채택해 animate 메서드를 구현해줍니다.

    중요한 점은 time과 duration 비교값이 없으면 한없이 애니메이션이 지속되기에 코드를 추가해줘야 합니다.

    duration으로 준 시간 만큼 애니메이션을 잡아먹습니다.

     

    Haptic Feedback

    햅틱 피드백을 통해 촉각 반응을 제공하여 주의를 끌고 작업 및 이벤트를 강화할 수 있습니다.

    이를 위해 sensoryFeedback API를 이용할 수 있습니다.

    .sensoryFeedback(.increase, trigger: sightingCount)

    해당 피드백을 연결하여 종류와 동작할 시기 즉, 트리거만 지정해주면 됩니다.

     

    Visual Effect Modifier

    새롭게 제공하는 시각 효과 모디파이어들을 이용해 더욱 좋은 경험의 애니메이션을 줄 수 있습니다.

    화면 초점 이동에 따라 애니메이션을 주는것이 있다고 가정해볼께요.

    그럼 내부 뷰는 CGPoint 타입의 focalPoint 프로퍼티를 가지기만 하면됩니다.

    그리고 각 뷰에서 visualEffect를 이용해 적절한 애니메이션을 줄 수 있어요.

    그런 다음 요걸 보여주는 뷰에서 

    이렇게 시뮬레이션의 포인트를 넘겨주기만 하면 됩니다.

    coordinateSpace로 좌표 공간을 넘겨주죠.

    그럼 이렇게 초점에 따라 해당하는 초점의 뷰에서 애니메이션이 이뤄집니다.

    이전처럼 GeometryReader를 사용하지 않아도 충분합니다.

     

    Text Style

    SwiftUI에서 이제 ShaderLibrary라는 구조체가 생겨 텍스트를 좀 더 자유롭게 꾸밀 수 있습니다.

    텍스트에 대해 줄무늬를 넣거나 각도를 조정하는 등 Metal shader 기능을 사용해 SwiftUI 모양 스타일로 직접 전환할 수 있어요.

     

    Symbol Effect

    이제는 SFSymbol에 대해서 애니메이션을 쉽게 제공해줄 수 있습니다.

    바로 symbolEffect 모디파이어를 통해서 말이죠!

    Image(systemName: "pencil.tip")
      .symbolEffect(.pulse)

    effect는 굉장히 다양하니 자유롭게 쓸 수 있습니다.

     

    Typesetting language

    각 언어마다 타이포그래피가 달라 크기를 조정해야 할 수 있습니다.

    예를들어 태국어와 같은 언어의 경우 더 긴 문자 형식을 사용합니다.

    그렇기에 앱이 여러 지역에서 잘 작동하도록 도와주는 도구가 있습니다.

    Text(person.name)
      .typesettingLanguage(.init(languageCode: .thai))

    위와 같이 typesettingLanguage 모디파이어를 이용해줄 수 있습니다.

    이로 인해 언어마다 타이포가 다르더라도 잘 맞춰줄 수 있죠!

     

    이렇게 SwiftUI의 애니메이션 기능들이 너무 많아지고 점차 발전하고 있음에 신기하네요🕺🏻

     

    그럼 마지막으로 더 발전된 인터렉션들에 대해 살펴보겠습니다.

     

    Enhanced interactions

    프레임워크 전반에 걸쳐 스크롤 뷰 개선, 포커스 및 키보드 입력 개선, 버튼 및 메뉴와 같은 컨트롤의 보다 심층적인 커스텀을 통해 뛰어난 상호 작용을 제공하는 기능을 이번 버전에서 향상했습니다.

    상호작용이 핵심인것 같네요!

     

    Scroll Transition Effects

    스크롤 뷰에서의 트랜지션 효과들이 나타났습니다.

    스크롤이 되어 화면에서 사라질때 효과를 줘볼까요?

    PersonCardView(person: person)
      .scrollTransition { content, phase in
        content
          .scaleEffect(phase.isIdentity ? 1 : 0.6)
          .opacity(phase.isIdentity ? 1 : 0)
      }

    스크롤 뷰 내 카드뷰가 있다고 쳐볼께요.

    생겨난게 scrollTransition 모디파이어입니다.

    각 스크롤뷰에 담긴 해당 하위 뷰가 스크롤에서 사라질때 ScrollTransitionPhase 값을 통해 크기를 작게 그리고 점차 흐려지게 해줍니다.

     

    한번 돌려볼까요?

     

    보시는것처럼 최상단으로 사라질때 크기와 투명도가 점차 조절되는 트랜지션 효과를 볼 수 있습니다.

     

    Custom Transition Effects

    이제 커스텀한 트랜지션 효과를 만들기 쉽습니다.

    한번 만들어볼까요?

    struct ContentView: View {
      @State private var transitionBtnClicked: Bool = false
      
      var body: some View {
        VStack {
          if transitionBtnClicked {
            Rectangle()
              .fill(.green)
              .frame(width: 100, height: 100)
              .transition(GreenTransition())
          }
          
          Button("Let's Transition") {
            withAnimation(.bouncy) {
              transitionBtnClicked.toggle()
            }
          }
        }
      }
    }
    
    struct GreenTransition: Transition {
      func body(content: Content, phase: TransitionPhase) -> some View {
        content
          .rotation3DEffect(
            .init(degrees: phase.value * (phase == .willAppear ? 90 : -90)),
            axis: (x: 1.0, y: 0.0, z: 0.0)
          )
      }
    }

    Transition 프로토콜을 채택해 body에 원하는 트랜지션 효과를 구현해줍니다.

    그리고 사용부에서 transition 모디파이어에 담아 구현해주면 끝!

    참 쉽죠!?

     

    ScrollView Layout Enhancements

    또한 스크롤뷰의 레이아웃을 컨트롤하기에 강화되었습니다.

    containerRelativeFrame를 이용해볼 수 있어요.

    .safeAreaInset(edge: .top) {
      ScrollView(.horizontal) {
        HStack {
          ForEach(colors, id: \.self) { color in
            TopCardView(color: color)
              .containerRelativeFrame(
                .horizontal,
                count: 5,
                span: 2,
                spacing: 10
              )
          }
        }
        .scrollTargetLayout()
      }
      .scrollTargetBehavior(.viewAligned)
    }

    count는 화면을 분할할 청크 수를 지정합니다.

    span은 각 뷰가 차지해야하는 청크 수입니다.

    각 스크롤 시 해당 뷰를 영역에 맞게 자동 싱크 맞추면서 고정하기 위해

    scrollTargetLayout을 HStack에 지정해주고 ScrollView에서 scrollTargetBehavior를 구현해줍니다.

     

    그럼 동작을 볼까요?

     

    의도대로 구현되네요🙌

     

    Scroll Position

    이제 SwiftUI에서 스크롤 포지션을 쉽게 다룰 수 있습니다.

    단순하게 스크롤 뷰에서 아래와 같이 scrollPosition 모디파이어를 붙여줍니다.

    .scrollPosition(id: $scrollPosition)

    그 다음 해당 scrollPosition 값을 사용해 원하는 뷰나 액션을 취해줄 수 있죠!

     

    HDR Images

    이제 높은 동적 범위로 콘텐츠 렌더링을 지원합니다.

    allowedDynamicRange 모디파이어를 이용해 더 높게 렌더링 할 수 있습니다.

    AsyncImage(url: URL(string: "https://i.pravatar.cc/150?u=fake@pravatar.com"))
      .allowedDynamicRange(.high)

    일반적으로 드물게 사용하는것이 좋고 이미지가 단독일때 사용하는것이 좋습니다.

    많은 리스트에서 이러한 동작을 일으키면 부담이 되니까요.

     

    Accessibility Zoom

    이미지를 줌할때 접근성이 더욱 좋아졌습니다.

    accessibilityZoomAction 모디파이어를 사용하면 됩니다.

    이를 통해 보조 접근성을 설정하지 않아도 동일한 기능을 사용할 수 있죠!

    AsyncImage(url: URL(string: "https://i.pravatar.cc/150?u=fake@pravatar.com"))
      .scaleEffect(zoomLevel)
      .accessibilityZoomAction{ action in
        switch action.direction {
        case .zoomIn:
          zoomLevel += 1.0
        case .zoomOut:
          zoomLevel -= 1.0
        }
      }

     

    Static member syntax for custom colors

    이제 에셋 카탈로그에 컬러들을 넣는다면 Static 멤버로 취급하여 접근할 수 있습니다.

    자동완성 등 편리하게 사용할 수 있죠.

    개인적으로 너무 좋네요... R.Swift나 SwiftGen의 강력한 기능 중 하나인 자동완성을 이제야 넣어주다니 ㅎ..

     

    Bordered Button Shapes

    버튼에 대해서도 새로운 BorderShape 모디파이어를 제공합니다.

    버튼에 대해 테두리 모양 스타일을 지정해줄 수 있죠.

    Button { 
      // action
    } label: {
      // label
    }
    .buttonStyle(.bordered)
    .buttonBorderShape(.roundedRectangle)

    기존에는 shape를 직접 구현해줘야했는데 기본적으로 제공해주는 인자들이 많아졌네요🕺🏻

    그 외 springLoadingBehavior 모디파이어등도 새로 생겼습니다.

     

    UnevenRoundedRectangle

    직역하면 고르지 않은 직사각형인데 모서리를 자유롭게 원하는 부분만 라운드 해줄 수 있습니다.

    UnevenRoundedRectangle(topLeadingRadius: 10)
      .fill(.green.gradient)
      .frame(width: 200, height: 200)
      
    Rectangle()
      .fill(.green.gradient)
      .frame(width: 200, height: 200)
      .clipShape(.rect(topLeadingRadius: 10))

     

    모서리들에 대해 커스텀하게 여러개도 설정할 수 있구요!

    아래와 같이 Rectangle로도 충분히 가능합니다.

     

    마무리

    으아.. 애니메이션 너무 좋아졌네요👍

    데이터 플로우도 간단해지고 심지어 SwiftData까지...👍

    지금까지 학습한 코드들은 아래 제 깃헙에서 확인할 수 있어요!

    https://github.com/GREENOVER/playground/tree/main/SwiftUI5

     

    GitHub - GREENOVER/playground: 학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터

    학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터. Contribute to GREENOVER/playground development by creating an account on GitHub.

    github.com

     

    참고 자료

    https://developer.apple.com/videos/play/wwdc2023/10148/

     

    What's new in SwiftUI - WWDC23 - Videos - Apple Developer

    Learn how you can use SwiftUI to build great apps for all Apple platforms. Explore the latest updates to SwiftUI and discover new scene...

    developer.apple.com

    https://www.youtube.com/watch?v=1UHShfxysPM 

Designed by Tistory.