-
Swift 6으로 앱을 마이그레이션하기
기존 샘플 앱의 업데이트를 따라 Swift 6 마이그레이션 과정을 직접 경험해 보세요. 증분 마이그레이션 방법을 모듈별로 설명하고, 컴파일러로 데이터 레이스 위험이 있는 코드를 식별하는 방법을 공유합니다. 명확한 분리 경계를 보호하고 공유 가변 상태에 대한 동시 접근을 제거하는 다양한 기술을 배워보세요.
챕터
- 0:00 - Introduction
- 0:33 - The Coffee Tracker app
- 0:45 - Review the refactor from WWDC21
- 3:20 - Swift 6 and data-race safety
- 4:40 - Swift 6 migration in practice
- 7:26 - The strategy
- 8:53 - Adopting concurrency features
- 11:05 - Enabling complete checking in the watch extension
- 13:05 - Shared mutable state in global variables
- 17:04 - Shared mutable state in global instances and functions
- 19:29 - Delegate callbacks and concurrency
- 23:40 - Guaranteeing data-race safety with code you don’t maintain
- 25:51 - Enabling the Swift 6 language mode in the watch extension
- 26:35 - Moving on to CoffeeKit
- 27:24 - Enabling complete checking in CoffeeKit
- 27:47 - Common patterns and an incremental strategy
- 29:55 - Global variables in CoffeeKit
- 31:05 - Sending an array between actors
- 33:53 - What if you can’t mark something as Sendable?
- 35:23 - Enabling the Swift 6 language mode in CoffeeKit
- 35:59 - Adding a new feature with guaranteed data-race safety
- 40:43 - Wrap up and the Swift 6 migration guide
리소스
관련 비디오
WWDC21
-
다운로드
안녕하세요, 저는 Swift 팀의 Ben입니다 기존 애플리케이션에서 Swift 6 언어 모드 활성화 방법을 안내해 드리겠습니다 Swift 6가 여러분의 앱을 경합이 발생할 수 있는 상태에서 보호하는 방법을 살펴보고 이러한 변경 사항을 앱에 점진적으로 도입하는 기술과 아직 Swift의 동시성 보장을 인식하지 못하는 프레임워크와의 상호작용을 처리하는 방법을 살펴볼 것입니다 제가 이 세션에서 사용할 것은 하루 동안의 커피 소비량을 추적하는 간단한 앱과 시계 페이스에 현재 카페인 수치를 보여 주는 컴플리케이션입니다 WWDC 2021에서 Swift 동시성을 처음 소개해 드렸을 때 Swift의 새로운 동시성 모델을 채택하는 방법을 안내해 드렸죠 그 강연에서 깔끔해 보이는 앱 아키텍처가 때때로 동시성의 복잡성을 숨기고 있다는 것을 보셨을 겁니다 뷰와 모델만 보았을 때는 모든 것이 정돈되어 보입니다 하지만 동시성이 관리되는 방식을 들여다보면 상황이 전혀 다릅니다 원래 앱에는 코드가 실행될 수 있는 동시 대기열이 3개 있었습니다 UI와 모델에서의 작업은 메인 대기열에서 수행되었습니다 백그라운드에서 작업을 수행하는 디스패치 대기열도 있었습니다 마지막으로 HealthKit에서 결과를 반환하는 것과 같은 완료 핸들러로의 특정 콜백은 임의의 대기열에서 수행되었습니다 유형은 잘 정리되어 있었지만 앱 전체에서 동시성이 정리된 방식은 그렇게 명확하지 않았습니다
어떤 대기열이 왜 코드를 실행하는지 그 이유가 여러 곳의 유형에서 명확하지 않게 엮여 있었습니다
Swift 동시성을 채택하는 것으로 이러한 임기응변식 동시성 아키텍처에서 벗어날 수 있었습니다 바로 이런 모습으로요 UI 뷰와 모델은 메인 액터라는 곳에서 실행되고 백그라운드 작업은 전용 액터에서 수행되도록 설정했습니다 액터는 서로 통신하기 위해 thread-safe value types와 Swift의 async/await 기능을 사용했습니다 빌드를 완료했을 때 동시성 아키텍처는 유형 아키텍처만큼이나 명확하고 설명하기 쉬웠습니다 하지만 한 가지 문제가 있었습니다 아키텍처 개선을 위해 리팩터링을 할 때 데이터 경합을 피하기 위해 프로그래머인 저에게 여전히 많은 책임이 있었죠 모든 지침을 따랐고 액터 간의 통신을 위해 값 유형을 사용했지만 굳이 그럴 필요가 없었습니다 예를 들어 클래스와 같은 참고 유형을 가져와서 한 액터에서 다른 액터로 전달할 수도 있었죠 참고 유형을 사용하면 공유 가변 상태를 전달할 수 있으며 이렇게 하면 액터가 공유 상태에 동시에 액세스하도록 허용하여 액터가 제공하는 상호 배제를 깨뜨릴 수 있습니다 따라서 한 액터에서 다른 액터로 클래스 인스턴스를 보내도 데이터 경합이 발생하여 프로그램이 충돌하거나 사용자 데이터가 손상될 수 있습니다 여기가 Swift 6의 이점을 살릴 수 있는 부분입니다
Swift 6 언어 모드에서는 데이터 격리가 완전히 시행됩니다 컴파일러는 작업과 액터 사이에서 이러한 우발적인 상태 공유가 일어나는 것을 방지하여 리팩터링을 수행하거나 앱에 새로운 기능을 추가할 때 새로운 동시성 버그가 생길 걱정을 하지 않아도 됩니다
Swift 6 언어 모드는 기존 및 신규 프로젝트 모두에서 사용 가능합니다 이 모드를 채택하면 컴파일 시 동시 코드의 실수를 포착해 앱의 품질을 크게 향상시킬 수 있습니다 특히 유용한 경우는 재현하기 어려운 충돌이 발생하여 데이터 경합의 위험을 체계적으로 제거하려는 경우입니다 또한 더 많은 동시성을 적극적으로 통합하여 응답성과 성능을 개선하려 한다면 Swift 6 모드를 채택하여 이러한 변경으로 새 데이터 경합이 발생할 위험을 방지할 수 있습니다
공용 Swift 패키지를 유지관리하는 경우 가능한 한 빨리 Swift 6를 채택하는 것이 코드 베이스를 마이그레이션하려는 사용자에게도 도움이 될 것입니다 동일하게 Swift 6를 채택한 종속성을 기반으로 빌드하여 이점을 누릴 수 있을 것입니다 누구나 Swift 6 채택 과정을 따라갈 수 있는 방법을 swiftpackageindex.com의 인기 패키지에서 확인할 수 있습니다 오늘은 채택이 실제로 진행되는 모습을 살펴보겠습니다 CoffeeTracker 애플리케이션으로 Swift의 데이터 격리를 활성화해 보겠습니다 이 작업을 단계별로 진행하고 컴파일러가 제공하는 몇 가지 지침을 통해 Swift가 CoffeeTracker에 데이터 경합이 없도록 보장하기 위해 변경해야 할 부분을 살펴볼 것입니다 현재 제 앱에는 실제로 데이터 경합이 없다고 생각합니다 그리고 여러분의 코드도 마찬가지일 가능성이 높습니다 기존 코드의 데이터 경합을 이미 대부분 제거하셨을 수 있습니다 수년간의 개선 작업, 버그 리포트 메인 스레드 검사기 Thread Sanitizer 등을 통해서요 데이터 경합 안전성의 진짜 가치는 동시성의 이점을 더 활용하기 위해 새로운 기능을 추가하거나 기존 코드를 리팩터링 하는 과정에서 새로 작성하는 코드의 버그로부터 보호하는 것입니다 데이터 경합 안전성으로 동시성을 안심하고 활용할 수 있습니다 새로운 데이터 경합이 발생하여 충돌을 재현할 수 없는 상황에서 나중에 그 부분을 찾아내거나 추측성 수정을 할 걱정을 하지 않아도 됩니다
지난 번 세션 이후로 커피 추적 앱은 대성공이었으며 저희는 새로운 기능 추가를 위해 팀을 확장했습니다
그 일환으로 앱의 일부를 새 프레임워크 CoffeeKit에 팩터링했고 이제 일부 코드는 그 안에 있습니다 팀은 앱에 새로운 기능을 추가할 생각이 가득이지만 그 전에 Swift 6로 업데이트하여 새로운 기능을 추가할 때 새로운 동시성 버그가 발생하지 않도록 할 계획입니다 방금 Xcode 16을 다운로드하고 열었습니다 이제 새로운 Swift 6 컴파일러와 watchOS 11용 최신 SDK가 있으니 앱을 빌드해 보겠습니다
정말 잘 빌드되었네요 업데이트할 필요가 없겠습니다 이는 앱에 데이터 경합 가능성이 없기 때문이 아닙니다 아직 Swift 6 언어 모드를 활성화하지 않았기 때문입니다 이전 버전과 마찬가지로 Swift 6는 소스 호환성을 보장합니다 아주 사소한 변경 사항을 제외하고 항상 새로운 컴파일러로 앱을 빌드해야 합니다
이제 최신 Xcode로 앱을 빌드했으니 다음 단계로 데이터 격리를 완벽하게 적용하는 Swift 6 모드를 활성화해 보겠습니다 이를 준비하기 위해 MainActor와 Sendable을 살펴보고 감사를 진행한 뒤 컴파일러 진단을 활성화할 수 있습니다 하지만 이렇게 하면 새로운 Swift 컴파일러의 이점을 놓치게 됩니다 컴파일러 진단은 수정이 필요한 부분을 안내합니다 코드에서 잠재적 버그를 지적하는 짝꿍 프로그래머라고 생각하면 됩니다 이렇게 하면 마이그레이션 프로세스에 구조를 추가하는 데 도움이 됩니다 이제 단계별 프로세스를 따라서 코드의 각 대상을 마이그레이션하겠습니다 각 대상에 대해 다음과 같은 단계를 따르겠습니다 먼저 전체 동시성 검사를 활성화합니다 이는 모듈별 설정으로 프로젝트를 Swift 5 모드로 유지하되 Swift 6의 강제 데이터 격리로 인해 실패할 수 있는 모든 코드에 대해 경고를 활성화합니다 이렇게 진행하면서 해당 대상에 대한 모든 경고를 해결합니다
이 작업이 완료되면 Swift 6 모드를 활성화합니다 모든 변경 사항이 잠기고 이후 리팩터링이 안전하지 않은 상태로 회귀하는 것을 방지할 수 있습니다 그리고 다음 대상으로 이동하여 프로세스를 반복합니다 마지막으로 모드가 활성화되면 돌아가서 전체 앱에 대한 리팩터링을 진행하고 아키텍처를 변경하여 안전하지 않은 옵트아웃을 취소하거나 코드를 더 멋지게 만들 수 있는 리팩터링을 할 수 있습니다 리팩터링에 대한 조언을 드리자면 중요한 리팩터링과 데이터 경합 안전성을 한 번에 모두 진행하려는 유혹을 뿌리쳐야 합니다 한 번에 하나씩 하세요 두 가지를 동시에 하려고 하면 한 번에 너무 많은 변화가 일어나서 다시 되돌아가야 할 수도 있습니다
여기에서는 Swift 동시성 사용을 위해 이미 리팩터링한 앱에서 Swift 6를 활성화하는 단계에만 집중하겠습니다 따라서 전체 검사 활성화부터 시작하겠습니다 전체 검사로 무엇을 할 수 있을까요? 앱에서 이미 Swift 동시성을 사용하고 있다면 Swift의 동시성 기능을 도입하면서 발생한 동시성 문제에 대해 Swift 컴파일러에서 경고나 오류를 본 적이 있을 것입니다 예를 들어 여기에서는 새로운 델리게이트를 추가하고 있습니다 제 카페인 수치가 걱정스러울 정도로 낮아지면 CoffeeKit에 추가되는 콜백을 수신하는 용도죠 이 델리게이트는 값을 제 SwiftUI 뷰에 다시 게시할 테니 메인 액터에 격리하고 싶습니다 따라서 @MainActor를 맨 위에 추가하여
모든 메서드와 속성 액세스가 메인 스레드에서 이루어지도록 할 수 있습니다 하지만 이렇게 하면 코드 아래쪽의 프로토콜 구현에서 오류가 발생합니다
잘 살펴보니 “메인 액터로 격리된 인스턴스 메서드 ‘caffeineLevel(at:)’가 비격리된 프로토콜 요구 사항을 충족할 수 없음”이라고 하네요 이 CaffeineThresholdDelegate 프로토콜은
이제 호출되는 방식을 보장하지 않습니다 CoffeeKit 내부에 있으며 아직 Swift 6로 업데이트되지 않았습니다 하지만 Recaffeinater 유형을 여기에 맞추고 메인 액터에 제한시켰습니다 이 유형의 메서드는 메인 액터에서 실행될 것이므로 항상 메인 액터에서 호출되는 것이 보장되지 않는 프로토콜을 따를 수는 없습니다 이 문제는 곧 다시 돌아와서 해결하겠습니다 하지만 이것은 Swift 컴파일러가 생성하는 오류의 예시로 여러분이 특정 유형을 메인 액터에서 호출되도록 설정했기 때문입니다 이전 세션에서 Coffee Tracker에서 Swift 동시성 채택하기를 보셨다면 앱의 여러 곳에서 동시성을 도입하면서 이러한 문제가 발생하는 것을 보셨을 것입니다
타겟의 빌드 설정에서 엄격한 검사를 활성화하면 전체 모듈이 가능한 경합 조건을 검사하도록 설정할 수 있습니다 이제 이를 활성화하고 어떻게 되는지 보겠습니다 Swift의 데이터 격리는 대상별로 활성화할 수 있으며 제 앱에는 주요 대상이 두 개입니다
UI 레이어가 있는 WatchKit 확장과 카페인을 추적하고 이를 HealthKit 라이브에 저장하는 비즈니스 로직이 작동하는 프레임워크 CoffeeKit입니다
먼저 Watch 확장에서 전체 검사를 활성화하겠습니다 여기에는 두 가지 이유가 있습니다 첫째, 동시성 검사 활성화가 더 간단한 경우가 많기 때문입니다 대부분의 UI 레이어는 메인 스레드에서 실행되며 SwiftUI 또는 UIKit와 같은 API를 사용하는데 이는 그 자체로 메인 스레드에서 작업을 수행하는 것을 보장합니다 또 다른 이유는 엄격한 동시성을 활성화할 때 아직 Swift 동시성을 위해 업데이트되지 않은 다른 모듈로 작업하는 경우가 많기 때문입니다 영영 업데이트되지 않을 C 라이브러리를 사용 중일 수도 있고 또는 프레임워크나 패키지 모듈이 Swift 6로 업데이트 예정은 있지만 아직인 경우일 수도 있습니다 물론 여기에는 저희의 자체 CoffeeKit 프레임워크도 포함됩니다 일단 시작해 보면 이런 하향식 접근이 왜 도움이 되는지 알 수 있을 겁니다 첫 번째 단계로 확장으로 이동하여 설정으로 이동해 보겠습니다 그리고 Swift 동시성 검사 설정을 검색하여
이렇게 하면 컴파일러가 동시성 안전을 확인할 수 없는 코드에 대해 경고를 표시하기 시작합니다 이는 경고일 뿐이며 프로젝트는 계속 빌드, 진행됩니다 이제 빌드를 해봅시다
그러자 앞서 소개한 경고 외에도 몇 가지 경고가 더 표시됩니다 한번 살펴보겠습니다
첫 번째 문제는 Swift 6에서 가장 흔히 발생하는 문제 중 하나인 ‘logger’ 변수에 관한 것입니다
logger 인스턴스가 전역 변수로 선언되어 있습니다 전역 변수는 공유 가변 상태의 소스로 어떤 스레드에서 실행되어도 프로그램의 모든 코드가 동일한 변수를 읽고 쓸 수 있습니다 따라서 이는 데이터 경합이 일어나기 쉬운 소스가 될 수 있으며 안전하게 만들어야 합니다 몇 가지 방법이 있습니다 실제로 문제를 펼쳐보면
컴파일러가 몇 가지 방법을 추천하는 것을 볼 수 있습니다
첫 번째는 가장 쉬운 방법으로 읽기 전용으로 만드는 것입니다 Logger는 Sendable 유형입니다 즉 let 변수로 선언하면 여러 스레드에서 사용할 때 데이터 경합을 일으킬 수 없습니다 따라서 이 var을 let로 전환합니다 그리고 다시 빌드합니다
해당 문제가 사라집니다 이것이 올바른 수정 방식이지만 다른 옵션도 있었습니다
이 변수가 불변하는 것이 아니라 나중에 값이 업데이트되었으면 합니다 그래서 let이 아닌 var로 유지하고 싶습니다
또 다른 방법은 이 전역 변수를 전역 액터에 묶는 것입니다 저는 UI 레이어에 있고 아마 제 모든 로깅이 메인 액터에서 일어날 것입니다 따라서 이 전역 변수를 @MainActor로 애노테이션 처리하면
네, 모든 로깅 사용이 메인 액터를 출처로 하여 이 방법으로도 경고가 제거됩니다
마지막 방법은 컴파일러가 강제할 수 없는 다른 외부 메커니즘으로 이 변수를 보호하는 방법입니다 디스패치 대기열로 모든 액세스를 보호하고 있을 수도 있습니다 이 경우 nonisolated(unsafe) 키워드를 사용할 수 있습니다
Swift에서 ‘unsafe’라는 단어가 쓰이는 다른 경우와 마찬가지로 이 변수에 대한 안전성 보장은 여러분의 책임이 됩니다 이 방법은 최후의 수단이 되어야 하며 대신 Swift의 컴파일 시간 보장을 사용하는 것이 가장 좋습니다 하지만 컴파일러가 모든 것을 알 수는 없으므로 이러한 경우 nonisolated(unsafe)를 선택해 사용할 수 있습니다 이러한 경우의 예시로는 나중에 돌아와서 이 코드를 리팩터링하고 싶을 때 이 변수를 액터로 옮겨서 안전하게 사용되고 있는지 컴파일러가 확인하게 하는 경우입니다 하지만 지금은 nonisolated(unsafe)로 표시하고 다음 경고로 넘어갈 수 있습니다 이 경우는 그런 경우가 아니므로 다시 이 변수를 let로 선언하는 것이
가장 좋은 선택입니다 이제 여기 이 이니셜라이저는 무엇인지 궁금하실 겁니다
전역을 초기화하는 부분이죠 언제 실행될까요? 스레드 안전을 이해하기 위해 알아야 할 중요한 사항 아닌가요?
Swift의 전역 변수는 지연 초기화됩니다 이 값은 처음 사용할 때 즉 CoffeeTracker가 무언가를 처음 로깅할 때 초기화됩니다 이는 C 및 Objective-C와 비교했을 때 정말 중요한 차이점입니다 해당 언어에서는 전역 변수가 시작 시 초기화됩니다 그리고 이는 프로그램 시작 시간에 아주 나쁜 영향을 줄 수 있습니다 Swift의 지연 초기화는 이러한 문제를 방지하여 앱이 더 빠르게 사용 가능한 상태가 될 수 있도록 합니다 하지만 지연 초기화도 경합을 유발할 수 있습니다 두 개의 스레드가 동시에 이 전역 변수를 사용하여 첫 로깅을 시도하면 어떻게 될까요? logger가 두 개 생길까요? 걱정 마세요. Swift에서는 전역 변수가 원자적으로 생성되도록 보장됩니다 두 개의 스레드의 Logger에 대한 첫 액세스가 동시에 일어나려고 하면 한 스레드만 초기화되고 다른 하나는 대기 중으로 차단됩니다 이제 이 문제는 해결되었으니 다음 문제를 살펴봅시다
WKApplication의 공유 인스턴스에 액세스하는 코드가 있습니다 이 전역 인스턴스 메서드는 메인 액터에 격리된 메서드의 예시입니다
여기서 가장 먼저 주목할 점은 액터 격리 상태에 대한 호출이 암시적으로 비동기적이라는 점입니다 즉, 이 함수가 비동기 함수라면 await를 사용해 메인 액터에서 이 전역 변수에 액세스할 수 있습니다 이 함수는 동기화되지 않으므로 비동기 함수로 표시하거나 새 작업을 시작해야 합니다 하지만 컴파일러가 다른 해결책을 제시하고 있으니
이 함수는 뷰의 메서드가 아닌 자유 함수이므로 기본적으로 메인 액터에 있는 것은 아닙니다 수정 사항을 적용하면
이제 이 메서드가 메인 액터에 격리되어 있고 빌드를 시도해 보면 성공합니다
두 번 호출되었는데 모두 메인 액터에 있는 메서드에서 호출되었습니다 이 함수가 메인 액터가 아닌 다른 곳에서 호출되었다면 이를 설명하는 컴파일러 오류가 발생했을 것이고 해당 호출자가 어떤 컨텍스트에서 호출되었는지 확인할 수 있었을 겁니다 하나는 SwiftUI 뷰입니다 그리고 다른 하나는 WKApplicationDelegate의 구현에 있습니다 살펴보도록 합시다
WKApplicationDelegate를 option-클릭하면 메인 액터에 연결되어 있는 프로토콜임을 확인할 수 있습니다 이는 이 프로토콜이 메인 액터에서만 호출됨을 보증합니다 WatchKit 프레임워크 또는 Swift 6 모드를 활성화하면 여러분의 코드에서 호출되겠죠
많은 델리게이트 및 SwiftUI 뷰와 같은 기타 프로토콜은 메인 액터에서만 작동하도록 설계되었는데 특히 Xcode 16과 함께 제공되는 최신 SDK에서 이렇게 애노테이션 처리되었습니다 가장 중요한 것은 여기에는 SwiftUI 뷰 프로토콜이 포함된다는 점입니다 이전에 엄격한 동시성 검사를 활성화했었다면 새 SDK에서는 필요 이상으로 많은 메인 액터 애노테이션을 추가해야 했고 애노테이션 중 일부는 제거 가능함을 알고 계실 것입니다 이제 델리게이트 콜백과 동시성을 조금 알아보겠습니다 이미 알고 계시겠지만 델리게이트나 완료 핸들러로부터 콜백을 받을 때마다 항상 먼저 이해해야 하는 것이 해당 콜백에서 동시성이 무엇을 보장하는지입니다 일부 콜백은 문서에 모든 콜백이 항상 메인 스레드에 있다고 명시되어 있을 수 있습니다 많은 UI 프레임워크가 이를 보장하며 이것이 Watch 익스텐션의 이 뷰 레이어 작업에서 무언가를 메인 액터로 표시할 때 경고가 많이 발생하지 않는 이유 중 하나입니다
반면에 일부 델리게이트는 정반대로 콜백이 어떻게 호출될지 보장하지 않습니다 임의의 스레드나 대기열에서 호출될 것이라고 하죠 이런 델리게이트가 적합한 콜백은 앱의 백엔드로 들어올 가능성이 높은 콜백입니다 CoffeeTracker가 HealthKit에서 수신하는 콜백이 이와 비슷합니다 이러한 경우 올바른 대기열이나 액터로 다시 디스패치하거나 스레드에 안전한 방식으로 작업해야 합니다 이 접근 방식의 문제점은 각 델리게이트에 문서로만 캡처되는 자체 규칙이 있어 올바른 작업을 해야 하는 사용자에게 많은 부담을 준다는 것입니다 다시 호출을 받았을 때 위치가 어디일지 생각해야 하며 다음 로직을 수행하기 위해 어디 있어야 하는지 생각해야 합니다 이를 확인하는 것을 잊거나 올바른 위치에 다시 디스패치하는 것을 잊어버리면 데이터 경합이 쉽게 발생할 수 있습니다 더 나쁜 것은 콜백이 이미 구축되어 작동 중이고 항상 메인 대기열에 있었지만 항상 보장되는 것은 아니었다고 가정해 보겠습니다 나중에 그 프레임워크에서 일부 변경 사항이 발생하여 다른 대기열로 들어오는 중이라고 가정해 보겠습니다 하지만 UI 레이어는 메인 대기열에 있는 것에 의존하고 있었습니다 여기서 놓치고 있는 것은 로컬 추론으로 이는 제가 UI 레이어에서 작업하고 있을 때 앱의 다른 곳에서 코드가 변경되어 작업 중인 대기열이 변경되는 등의 이유로 쉽게 중단되지 않도록 보장하는 것입니다
Swift 동시성은 이 문제를 이러한 보장 또는 보장 없음을 명시하여 해결합니다 콜백이 다시 호출되는 방법을 지정하지 않으면 격리되지 않은 것으로 간주되어 특정 격리가 필요한 데이터에 액세스할 수 없습니다 반면에 콜백이 격리 보장을 제공하며 항상 메인 액터에서 다시 호출되는 경우 델리게이트 프로토콜 또는 콜백에 항상 메인 액터에 있다고 애노테이션 처리할 수 있으며 콜백 수신자는 이러한 보장을 신뢰할 수 있습니다 말이 나왔으니 첫 번째 경고로 돌아가 보겠습니다 메인 액터의 델리게이트 유형이 비격리 프로토콜을 준수할 수 없다는 경고였죠 여기에는 컴파일러가 제공하는 몇 가지 옵션이 있습니다 첫 번째는 메서드를 비격리된 것으로 선언하는 겁니다 즉, 메인 액터 격리 유형의 메서드임에도 불구하고 이 특정 메서드는 메인 액터와 격리되지 않습니다 이는 어디로 다시 호출할지 의도적으로 약속하지 않는 콜백의 경우 사용할 수 있는 방법입니다 물론 이것은 뷰이기 때문에 즉시 돌아가서 메인 액터 작업을 하고 싶을 것입니다 그렇게 하지 않으면 이 코드를 컴파일할 때 새로운 오류가 발생합니다 메인 액터에 의해 보호되는 뷰의 프로퍼티에 액세스하기 때문입니다
메인 액터에서 작업을 시작하면 이 문제를 해결할 수 있습니다
하지만 이 경우에는 이 콜백이 메인 액터에 있어야 하고 이 콜백이 메인 액터에서 실행되는 CoffeeKit 내부의 제 모델 유형에서 발생합니다
전체 코드베이스를 유지 관리하고 있는 경우라면 지금 바로 수정하면 됩니다 정의로 이동해서 CoffeeKit 내부의 프로토콜을 볼 수 있습니다 여기에 @MainActor로 애노테이션 처리해서 메인 액터에서 호출되도록 보장합니다 하지만 이렇게 전체 코드베이스를 관리하지 않는 경우도 있습니다 다른 팀에서 CoffeeKit을 관리하거나 다른 사람이 관리하는 패키지 또는 프레임워크에 의존할 수도 있습니다 그런 경우라고 가정해 보고 델리게이트 구현으로 돌아갑니다
이제 이 메서드가 메인 액터에서 호출되고 있다는 것을 알았습니다 방금 코드를 확인했거나 이 델리게이트에 대한 문서에서 읽었을 수도 있습니다
특정 액터에서 호출된다는 것을 확실히 알고 있다면 컴파일러에 이를 알리기 위해 사용할 수 있는 메서드가 있는데 바로 assumeIsolated입니다
코드가 작업을 시작하는 대신 MainActor.assumeIsolated를 작성합니다 이는 메인 액터에 비동기화하기 위해 새 작업을 시작하지 않습니다 Swift에게 이 코드가 이미 메인 액터에서 실행 중임을 알려줄 뿐입니다
코드의 어떤 부분에서는 메인 액터가 아닌 다른 곳에서 이 함수를 호출하는 것이 여전히 완벽하게 가능할 수 있습니다 이는 오늘날 Swift 함수가 메인 스레드에서 호출된다고 가정하는 것과 동일합니다 이를 방지하는 좋은 방법은 함수 내부에 여러분이 실제로 메인 액터에 있다는 어설션을 추가하는 것이며 assumeIsolated가 이 작업을 합니다 이 함수가 메인 액터에서 호출되지 않으면 함수에 트랩이 발생하고 프로그램 실행이 중지됩니다 트랩은 바람직하지 않은 현상이지만 사용자의 데이터를 손상시킬 수 있는 경합 상태보다는 낫습니다
메인 액터에서 호출되는 것으로 가정하는 델리게이트 프로토콜을 따르는 이 패턴은 약어가 있을 정도로 일반적입니다 따라서 변경한 내용을 취소하고
여기서 대신 프로토콜 적합성에 @preconcurrency를 작성합니다
이렇게 하면 직접 작성한 모든 내용이 이 뷰가 격리된 액터에서 호출되고 있다고 가정하고 그렇지 않으면 트랩이 발생합니다
이제 모든 동시성 경고를 제거했으니 이 대상에서 Swift 6를 활성화할 준비가 되었습니다 설정으로 이동합니다 이번에는 Swift Language Mode를 검색합니다
컴파일하면 오류와 경고 없이 빌드됩니다 이제 확장에서 전체 데이터 격리 검사를 잠궜으므로 앞으로 여기서 변경하는 모든 것은 컴파일러에서 전체 데이터 격리 검사를 수행하여 실수로 데이터 경합을 일으키지 않게 할 수 있습니다 좋습니다, 이제 확장이 Swift 6 모드에 있으니 CoffeeKit 대상으로 주의를 돌려보겠습니다 이제 이 대상에서 작업하고 있으니 델리게이트 프로토콜에 @MainActor 애노테이션 처리를 하겠습니다 한번 살펴보죠 메인 액터 애노테이션을 추가하고 다시 빌드하니 새 경고가 나오는군요
확장에 경고가 다시 나타나고 방금 추가한 @preconcurrency 속성에 관한 것입니다
이제 컴파일러는 이 프로토콜이 메인 액터에서 보장된다는 것을 알 수 있으므로 컴파일러가 표시하는 경고는 사전 동시성 속성이 더 이상 필요하지 않다는 내용입니다 따라서 제거하면 됩니다
됐습니다, 이 문제가 해결되었으니 이전과 동일한 루틴을 따라 전체 동시성 검사를 수행할 수 있습니다 프로젝트 설정으로 이동하여
경고가 더 늘어났네요 11개가 있습니다 간단한 프로젝트치고는 꽤 많은 경고입니다
이것은 일반적인 경험이 될 겁니다 전체 동시성 검사를 프로젝트에서 활성화하면 프로젝트에서 수백 개 어쩌면 수천 개의 경고가 생성됩니다 이런 일이 발생하면 참여 중인 프로젝트가 걱정될 겁니다 이때 지레 겁먹지 않는 것이 중요합니다
적은 수의 문제에서 수많은 경고가 파생되는 것은 일반적입니다 또한 이러한 문제 대부분은 빨리 해결되는 경우가 많습니다
따라서 처음으로 동시성 경고를 정리할 때 여러 가지 간단한 변경을 하고 메서드를 메인 액터에 배치하고 전역을 불변으로 바꾸고 이런 작업들을 통해 경고를 빠르게 줄일 수 있습니다 엄격한 검사를 처음 켰을 때 빨리 해결 가능한 경고들을 찾아서 조치를 취하는 것이 좋은 전략입니다 먼저 가장 간단한 수정으로 경고를 줄여나가세요 또한 수많은 문제의 근원이 되는 문제를 찾아내는 것도 좋은 방법입니다 코드 한 줄만 바꾸면 연쇄적인 문제 수백 개를 해결할 수 있기도 합니다 전체 검사 모드를 이전 버전의 Xcode에서 사용해 본 적이 있다면 최신 Xcode 베타 버전으로 시도해 보는 것도 좋습니다 최신 SDK에는 마이그레이션에 도움이 되는 애노테이션이 더 많이 있습니다 예를 들어, 이제 모든 SwiftUI 뷰가 메인 액터에 연결되므로 더 이상 뷰 유형에 메인 액터 애노테이션을 직접 추가할 필요가 없는 것입니다 사실 이러한 애노테이션 중 일부는 이미 추론되고 있으므로 제거할 수 있습니다
이렇게 해 보고 나면 처리하기 어려운 경고가 훨씬 줄어들 수 있습니다 또 한 가지 기억해야 할 것은 모든 문제를 한 번에 해결할 필요는 없다는 것입니다 릴리스를 출시해야 하거나 더 급한 변경 작업을 해야 하는 경우 설정으로 돌아가서 엄격한 검사를 다시 해제할 수 있습니다 경고를 줄이기 위해 변경한 모든 내용은 코드 베이스에 유효한 개선 사항으로 남게 되므로 계속 유지하며 확인할 수 있습니다 최소한의 검사만 해야 하는 상황이어도 가능합니다 후에 준비가 되면 다시 엄격한 검사로 문제를 해결할 수 있습니다
이 경우 경고를 살펴보기 시작하자 본 적 있는 패턴을 발견했습니다 이러한 전역 변수 중 몇 개는 var로 표시되어 있지만 모두 상수이거나 변경할 필요 없는 앞서 살펴본 logger와 같은 변수로 보입니다 이 모든 문제를 상당히 빠르게 해결할 수 있습니다 여기서 여러 줄의 코드를 수정하는 기술을 시도해보기 좋겠군요
작업이 실제보다 너무 쉬워 보인다는 인상을 드리고 싶진 않습니다 이것은 샘플 프로젝트일 뿐이며 실제 프로젝트에서는 경고가 더 많을 것입니다 하지만 실제 프로젝트를 진행하며 경험한 바에 따르면 이건 일반적인 경험입니다 쉽게 해결 가능한 문제도 많지만 해결하기 어려운 문제도 몇 가지 있습니다 마지막 오류를 살펴봅시다 서로 다른 액터들 사이에서 음료 배열을 전달하면서 발생한 오류군요 예를 들어 이 첫 번째 오류는 self.currentDrinks를 이 save 메서드로 보내면 데이터 경합 가능성이 있다고 합니다 저장 메서드는 다른 액터에 있는 메서드입니다 CoffeeData는 메인 액터에 있어야 합니다 SwiftUI ObservableObject이기 때문입니다
이 CoffeeDataStore 액터에 있습니다 디스크에서 저장 및 로드를 백그라운드에서 수행하는 액터입니다 경고를 다시 보면 모델에 보관하는 동일한 음료 배열을 메인 액터에 격리된 상태로 저장하도록 전송하고 있음을 알 수 있습니다
Drink가 참고 유형이라면 메인 액터와 저장 액터 모두 공유 가변 상태에 동시에 액세스할 수 있는 잠재적인 데이터 경합이 발생할 수 있습니다 이 문제를 해결하기 위해 Drink를 살펴보겠습니다 정의로 이동하면
몇 가지 불변 속성이 있는 구조체이며 속성이 모두 값 유형임을 알 수 있습니다 이런 점을 보았을 때 Sendable로 만들 수 있으며 음료를 한 액터에 저장한 다음 동일한 배열을 다른 액터로 전송하는 것이 완벽할 것입니다
만약 이것이 내부 유형이었다면 Swift는 이 유형을 자동으로 Sendable로 간주했을 것입니다 하지만 이것은 공용 유형이므로 CoffeeKit 외부에서 CoffeTracker 확장을 통해 공유합니다
Swift는 공용 유형의 전송 가능성을 자동으로 추론하지 않습니다 유형을 전송 가능으로 표시하는 것이 클라이언트에게 하는 일종의 보증이기 때문입니다 이 유형에는 지금은 변경 가능한 상태가 없지만 나중에 변경하고 싶을 수도 있습니다 전송 가능성을 미리 고정하고 싶지 않습니다 이러한 이유로 Swift는 공용 유형에 Sendable 적합성을 명시적으로 추가하도록 요구합니다
이 경우에는 기꺼이 그렇게 하겠습니다 참고로 이것은 한 줄 변경으로 여러 경고를 한 번에 제거할 수 있는 예시입니다 Drink 유형이 Sendable이어야 했던 다른 세 곳의 경고를 제거했습니다 대규모 프로젝트에서는 세 곳이 아니라 여러 프로젝트에서 수십 개의 경고가 발생할 수도 있습니다 이제 이 유형을 Sendable로 표시해 보겠습니다
그리고 여기에 전송 가능하지 않은 다른 유형이 있음을 알 수 있습니다
이 유형은 열거형일 뿐이므로 역시 전송 가능으로 표시합니다 하지만 만약 그렇지 않고 Objective-C 유형이라면 변경 가능한 상태를 참고 유형에 저장하기 때문에 절대 전송할 수 없다면 어떻게 할까요? 이 시점에서 안전성에 대해 선택을 해야 할 수도 있습니다 한 가지 옵션은 해당 유형에 대해 추론해 변경 가능한 참고 유형이더라도 NSCopying으로 생성된 새 복사본이므로 안전하다고 판단하는 것입니다 전송 가능하지 않더라도 해당 클래스를 보호하기 위해 변수를 비공개로 설정하고 전송 가능한 유형에 저장할 수 있다고 결정할 수도 있습니다 그렇게 하려면 다시 nonisolated(unsafe) 키워드를 사용합니다
그렇게 하면 이제 Drink 유형이 Sendable 애노테이션과 컴파일됩니다 이게 필수가 아닌 것을 알았으니 이를 취소하고
이제 CoffeeKit에서 모든 동시성 경고를 제거했으니 이 대상에서 Swift 6 모드를 활성화할 준비가 되었습니다 다시 설정으로 이동합니다
이번에는 Swift Language Mode를 검색합니다
빌드됩니다 이 시점에서 CoffeeTracker 앱 전체가 Swift 동시성에 의해 보호됩니다
이제 보호가 완료되었으므로 CoffeeTracker에 새로운 기능을 추가해 보겠습니다 사용자는 원하는 것은 커피를 마실 때 위치를 추적하여 해당 데이터를 마이닝하고 카페인 섭취 습관에 대한 주요 인사이트를 얻는 것입니다 그럼 이제 앱에 CoreLocation을 추가하는 방법을 살펴보겠습니다 CoffeeKit의 addDrink 메서드로 이동해 보겠습니다 그리고 음료를 추가하기 직전에 CoreLocation을 사용하여 사용자의 현재 위치를 가져올 것입니다 CoreLocation에는 현재 위치를 스트리밍하는 비동기 시퀀스가 있는데 이는 Swift 동시성과 아주 잘 어울리며 여기서 이를 사용하여 위치 결과를 반복하고 적절한 수준의 정확도를 얻게 되면 카페인 샘플에 위치를 할당할 수 있습니다 이 코드에 타임아웃을 추가하고 싶으시다면 Swift의 구조화된 동시성 및 취소 기능을 사용하면 됩니다 이 접근 방식에는 한 가지 문제가 있습니다
CoffeeTracker의 최소 배포 목표를 높여야 한다는 것입니다 아직 그럴 준비는 되지 않았고 watchOS 10으로 업데이트하고 싶지는 않지만 커피 마시는 위치를 추적하고 싶은 사용자들이 아직 있습니다 따라서 대신 델리게이트 콜백을 기반으로 하는 이전 CoreLocation API를 사용해야 합니다 Swift 동시성보다 이전 것이므로 사용하기 조금 더 까다로울 것입니다 여기 이 접근 방식의 정말 좋은 점은 이 코드가 일반 동기식 코드처럼 보인다는 것입니다 위치를 요청하고 들어오는 위치 업데이트 스트림을 충분히 정확한 위치를 얻을 때까지 반복하는 모든 작업이 배열에 새 음료를 추가하는 이 함수 내에서 가능합니다 반면 델리게이트 API를 사용하면 일부 상태를 저장하고 델리게이트 메서드가 실행될 때까지 기다리고 거기서부터 음료 값을 위치와 함께 계속 저장해야 합니다 따라서 이러한 새로운 API를 활용하기 위해 배포 목표를 상향하는 것이 좋습니다
하지만 그렇게 하고 싶지 않다면? 대신 이 새 코드를 제거하고
CoreLocation 델리게이트 오브젝트를 만들어 보겠습니다
CoreLocation에서 위치 업데이트를 수신할 수 있는 델리게이트 클래스의 기본 구현이 여기 있습니다 CoreLocation의 이 부분은 Swift 동시성보다 이전의 것입니다 따라서 규칙을 준수하기 위해 더 많은 작업을 해야 합니다 이 세션에서 살펴본 다른 모든 콜백들과 이 CLLocation 델리게이트는 약간 다릅니다 어떤 스레드에서 다시 호출될지 정적으로 보장하지 않습니다
지금까지는 항상 메인 스레드에서 호출되는 델리게이트 또는 임의의 스레드에서 호출되는 델리게이트에 대해 이야기했습니다 CoreLocationManager에 대한 문서를 보면 이 델리게이트가 호출되는 스레드는 CLManager를 생성한 스레드에 따라 결정된다는 보장이 있음을 알 수 있습니다 이는 약간의 도움 없이는 Swift에서 자동으로 적용할 수 없는 동적 속성입니다
이 경우 모델 유형에서 메인 액터의 이 정보를 사용하고 있습니다 따라서 가장 간단한 방법은 이 델리게이트도 메인 스레드에 있도록 하는 것입니다 Swift에서 이를 시행할 수 있는 도움을 받을 수 있는 모드로 돌아가려면 이 유형을 전적으로 메인 액터에 배치하면 됩니다
생성된다는 뜻입니다 따라서 델리게이트도 메인 액터에 들어올 것입니다
물론 이렇게 하면 컴파일러에서 익숙한 오류가 발생합니다 이 델리게이트 콜백이 메인 액터와 격리되지 않았다는 오류죠 우리는 이미 이 문제 패턴을 처리하는 방법을 확인했습니다
그런 다음 메인 액터에서 실행되어야 하는 코드를 MainActor.assumeIsolated 호출로 감쌉니다
빌드가 성공했습니다 이제 앱에서 현재 위치를 소싱하고 있으며 여전히 Swift 6에서 빌드 중입니다 지금까지 앱을 Swift 6 언어 모드로 마이그레이션하는 몇 가지 기술을 간략하게 살펴봤습니다 오늘 다루지 않은 시나리오가 많지만 더 많은 리소스가 준비되어 있습니다 먼저 이전 세션을 시청하시면서 Swift의 동시성 기능을 사용해 기존 앱을 현대화하는 방법을 살펴보십시오 이 코드를 Swift 6로 마이그레이션하는 과정의 대부분은 해당 세션에서 다룬 일부 리팩터링 덕분에 더 쉬워졌습니다 이전 마이그레이션에서 얻은 교훈을 바탕으로 Swift 6 언어 모드는 점진적 마이그레이션이 Swift 에코시스템 전반에 걸쳐 가능하도록 설계되었습니다 따라서 데이터 경합을 코드 변경 한 번에 하나씩 없앨 수 있습니다 커뮤니티에서 이러한 전환을 진행하면서 정적 및 동적 데이터 경합의 안전성 사이를 연결하는 기술의 극히 일부분을 살펴보았습니다 모든 전략과 더 많은 정보가 있는 가이드가 Swift.org/migration에 있습니다 직접 코드 작업을 하실 때 유용한 리소스가 되기를 바랍니다 시청해 주셔서 감사합니다!
-
-
9:08 - Recaffeinater and CaffeineThresholdDelegate
//Define Recaffeinator class class Recaffeinater: ObservableObject { var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //Add protocol to notify if caffeine level is dangerously low extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
9:26 - Add @MainActor to isolate the Recaffeinator
//Isolate the Recaffeinater class to the main actor class Recaffeinater: ObservableObject { var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 }
-
9:38 - Warning in the protocol implementation
//warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
9:59 - Understanding why the warning is there
//This class is guaranteed on the main actor... class Recaffeinater: ObservableObject { var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
12:59 - A warning on the logger variable
//var 'logger' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
13:38 - Option 1: Convert 'logger' to a 'let' constant
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:20 - Option 2: Isolate 'logger' it to the main actor
//Option 2: Annotate 'logger' with '@MainActor' if property should only be accessed from the main actor var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:58 - Option 3: Mark it nonisolated(unsafe)
//Option 3: Disable concurrency-safety checks if accesses are protected by an external synchronization mechanism nonisolated(unsafe) var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
15:43 - The right answer
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
17:03 - scheduleBackgroundRefreshTasks() has two warnings
func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() //warning: Call to main actor-isolated class method 'shared()' in a synchronous nonisolated context // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { //warning: Call to main actor-isolated instance method 'scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:)' in a synchronous nonisolated context error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
17:57 - Annotate function with @MainActor
func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
22:15 - Revisiting the Recaffeinater
//This class is guaranteed on the main actor... class Recaffeinater: ObservableObject { var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not //warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
22:26 - Option 1: Mark function as nonisolated
//error: Main actor-isolated property 'minimumCaffeine' can not be referenced from a non-isolated context nonisolated public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
23:07 - Option 1b: Wrap functionality in a Task
nonisolated public func caffeineLevel(at level: Double) { Task { in if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
23:34 - Option 1c: Explore options to update the protocol
public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
24:15 - Option 1d: Instead of wrapping it in a Task, use `MainActor.assumeisolated`
nonisolated public func caffeineLevel(at level: Double) { MainActor.assumeIsolated { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
25:21 - `@preconcurrency` as a shorthand for assumeIsolated
extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
26:42 - Add `@MainActor` to the delegate protocol in CoffeeKit
public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
26:50 - A new warning
//warning: @preconcurrency attribute on conformance to 'CaffeineThresholdDelegate' has no effect extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
27:09 - Remove @preconcurrency
extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
29:56 - Global variables in CoffeeKit are marked as `var`
//warning: Var 'hkLogger' is not concurrency-safe because it is non-isolated global shared mutable state private var hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. //warning: Var 'anchorKey' is not concurrency-safe because it is non-isolated global shared mutable state private var anchorKey = "anchorKey" // The HealthKit store. // warning: Var 'store' is not concurrency-safe because it is non-isolated global shared mutable state private var store = HKHealthStore() // warning: Var 'isAvailable' is not concurrency-safe because it is non-isolated global shared mutable state private var isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. // warning: Var 'caffeineType' is not concurrency-safe because it is non-isolated global shared mutable state private var caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! // warning: Var 'types' is not concurrency-safe because it is non-isolated global shared mutable state private var types: Set<HKSampleType> = [caffeineType] // Milligram units. // warning: Var 'miligrams' is not concurrency-safe because it is non-isolated global shared mutable state internal var miligrams = HKUnit.gramUnit(with: .milli)
-
30:19 - Change all global variables to `let`
private let hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. private let anchorKey = "anchorKey" // The HealthKit store. private let store = HKHealthStore() private let isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. private let caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! private let types: Set<HKSampleType> = [caffeineType] // Milligram units. internal let miligrams = HKUnit.gramUnit(with: .milli)
-
30:38 - Warning 1: Sending arrays in `drinksUpdated()`
// warning: Sending 'self.currentDrinks' risks causing data races // Sending main actor-isolated 'self.currentDrinks' to actor-isolated instance method 'save' risks causing data races between actor-isolated and main actor-isolated uses await store.save(currentDrinks)
-
32:04 - Looking at Drink struct
// The record of a single drink. public struct Drink: Hashable, Codable { // The amount of caffeine in the drink. public let mgCaffeine: Double // The date when the drink was consumed. public let date: Date // A globally unique identifier for the drink. public let uuid: UUID public let type: DrinkType? public var latitude, longitude: Double? // The drink initializer. public init(type: DrinkType, onDate date: Date, uuid: UUID = UUID()) { self.mgCaffeine = type.mgCaffeinePerServing self.date = date self.uuid = uuid self.type = type } internal init(from sample: HKQuantitySample) { self.mgCaffeine = sample.quantity.doubleValue(for: miligrams) self.date = sample.startDate self.uuid = sample.uuid self.type = nil } // Calculate the amount of caffeine remaining at the provided time, // based on a 5-hour half life. public func caffeineRemaining(at targetDate: Date) -> Double { // Calculate the number of half-life time periods (5-hour increments) let intervals = targetDate.timeIntervalSince(date) / (60.0 * 60.0 * 5.0) return mgCaffeine * pow(0.5, intervals) } }
-
33:29 - Mark `Drink` struct as Sendable
// The record of a single drink. public struct Drink: Hashable, Codable, Sendable { //... }
-
33:35 - Another type that isn't Sendable
// warning: Stored property 'type' of 'Sendable'-conforming struct 'Drink' has non-sendable type 'DrinkType?' public let type: DrinkType?
-
34:28 - Using nonisolated(unsafe)
nonisolated(unsafe) public let type: DrinkType?
-
34:45 - Undo that change
public let type: DrinkType?
-
35:04 - Change DrinkType to be Sendable
// Define the types of drinks supported by Coffee Tracker. public enum DrinkType: Int, CaseIterable, Identifiable, Codable, Sendable { //... }
-
36:35 - CoreLocation using AsyncSequence
//Create a new drink to add to the array. var drink = Drink(type: type, onDate: date) do { //error: 'CLLocationUpdate' is only available in watchOS 10.0 or newer for try await update in CLLocationUpdate.liveUpdates() { guard let coord = update.location else { logger.info( "Update received but no location, \(update.location)") break } drink.latitude = coord.coordinate.latitude drink.longitude = coord.coordinate.longitude } catch { }
-
38:10 - Create a CoffeeLocationDelegate
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
39:32 - Put the CoffeeLocationDelegate on the main actor
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() // This CLLocationManager will be initialized on the main thread manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } // error: Main actor-isolated instance method 'locationManager_:didUpdateLocations:)' cannot be used to satisfy nonisolated protocol requirement func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
40:06 - Update the locationManager function
nonisolated func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { MainActor.assumeIsolated { self.location = locations. last } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.