// publishMonthID는 publishDate로부터 파생된 임시 속성
@objc public var publishDate: Date? {
get {
willAccessValue(forKey: Name.publishDate)
defer { didAccessValue(forKey: Name.publishDate) }
return primitivePublishDate
}
set {
willChangeValue(forKey: Name.publishDate)
defer { didChangeValue(forKey: Name.publishDate) }
primitivePublishDate = newValue
primitivePublishMonthID = nil // publishDate의 setter 메서드는 primitivePublishMonthID를 무효화합니다. 이를 통해 publishMonthID의 getter 메서드가 현재 publishDate를 기반으로 값을 다시 계산할 수 있습니다.
}
}
@objc public var publishMonthID: String? {
willAccessValue(forKey: Name.publishMonthID)
defer { didAccessValue(forKey: Name.publishMonthID) }
// publishMonthID의 getter 메서드가 현재 publishDate를 기반으로 값을 다시 계산
guard primitivePublishMonthID == nil, let date = primitivePublishDate else {
return primitivePublishMonthID
}
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents([.year, .month], from: date)
if let year = components.year, let month = components.month {
primitivePublishMonthID = "\(year * 1000 + month)"
}
return primitivePublishMonthID
}
// publishMonthID가 publishDate와 연결되고 항상 최신 상태를 유지할 수 있다.
// PublishMonthID가 Swift에서 Key-Value Observing을 사용하는 경우,
// 다음 코드는 publishDate가 변경될 때 observation이 트리거되도록 보장
class func keyPathsForValuesAffectingPublishMonthID() -> Set<String> {
return [Name.publishDate]
}
Derived Attribute
Derived Attribute(파생 속성)을 사용하여 다른 값에서 하나의 값 파생
저장 공간보다 성능이 더 중요한 경우 사용
사용자가 managed context를 저장할 때만 업데이트 됨
예제 코드
canonical: 함수는 대소문자와 발음 구별 부호를 구분하지 않는 문자열 값을 반환
파생 속성은 사용자가 managed context를 저장할 때만 업데이트 되므로, title 속성을 저장하지 않고 변경하는 경우 canonicalTitle은 변경되지 않음
extension ViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let predicate: NSPredicate
if let userInput = searchController.searchBar.text, !userInput.isEmpty {
// Searching title with "diacritic insensitive" option gets the same result:
// predicate = NSPredicate(format: "title CONTAINS[cd] %@", userInput)
// However, searching canonicalTitle avoids doing diacritic insensitive comparison every time,
// and hence has better performance.
//
predicate = NSPredicate(format: "canonicalTitle CONTAINS[c] %@", userInput)
} else {
predicate = NSPredicate(value: true)
}
fetchedResultsController.fetchRequest.predicate = predicate
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("###\(#function): Failed to performFetch: \(error)")
}
tableView.reloadData()
}
}
Transformable Attribute
Transformable Attribute(변환 가능한 속성)을 사용하여 비표준 유형의 객체를 저장할 수 있음
예제 코드
타입을 Transformable로 설정하고, Data Model Inspector에서 Transformer 및 Custom Class 이름을 지정하여 구성
앱이 Core Data Stack을 로드하기 전에 코드로 Transformer을 등록
lazy var persistentContainer: NSPersistentContainer = {
// Register the transformer at the very beginning.
// .colorToDataTransformer is a name defined with an NSValueTransformerName extension.
ValueTransformer.setValueTransformer(ColorToDataTransformer(), forName: .colorToDataTransformer)
let container = NSPersistentContainer(name: "CoreDataAttributes")
container.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("###\(#function): Failed to load persistent stores:\(error)")
})
container.viewContext.automaticallyMergesChangesFromParent = true
SampleData.generateSampleDataIfNeeded(context: container.newBackgroundContext())
return container
}()
Date Type
Decimal Type
let value = UInt64(arc4random_uniform(9999))
book.price = NSDecimalNumber(mantissa: value, exponent: -2, isNegative: false)
cell.price.text = book.price?.description(withLocale: Locale.current) // ex) 98.14
<!-- Provide required visibility configuration for API level 30 and above -->
<queries>
<!-- If your app checks for SMS support -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="sms" />
</intent>
<!-- If your app checks for call support -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
</queries>
open ~/Library/flutter/packages/flutter_tools/gradle/flutter.gradle
/** For apps only. Provides the flutter extension used in app/build.gradle. */
class FlutterExtension {
/** Sets the compileSdkVersion used by default in Flutter app projects. */
static int compileSdkVersion = 33
/** Sets the minSdkVersion used by default in Flutter app projects. */
static int minSdkVersion = 16
/** Sets the targetSdkVersion used by default in Flutter app projects. */
static int targetSdkVersion = 33
// ...
}
let javascriptSourceCode = "MyFunction" // 실행할 자바스크립트
let userScript = WKUserScript(
source: javascriptSourceCode,
injectionTime: .atDocumentStart, // 스크립트를 웹페이지에 삽입하는 시간을 결정
// injectionTime: .atDocumentEnd,
forMainFrameOnly: true
)
let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)
[방법 2] 필요한 시점에 자바스크립트 실행
let javascriptSourceCode = "MyFunction" // 실행할 자바스크립트
webView.evaluateJavaScript(javascriptSourceCode) { result, error in
if let error = error {
print(error.localizedDescription)
} else if let result = result {
print(result)
}
}
While custom URL schemes are an acceptable form of deep linking, universal links are strongly recommended. For more information on universal links, see Allowing apps and websites to link to your content.
Apple 공식문서에서는 custom URL Scheme 방식보다는 Universal Link 방식을 강력히 권장합니다.
URI 스킴 방식
클라이언트 앱이 서버 앱을 호출한다고 가정합니다.
1. 서버 앱의 URL 형식을 정의합니다.
// ex) 사진 라이브러리 앱의 경우 표시할 앨범의 name이나 index를 포함하는 URL 형식을 정의합니다.
myphotoapp:albumname?name="albumname"
myphotoapp:albumname?index=1
2. 서버 앱에서 URL 스키마를 등록합니다.
[Target] - [Info] - [URL Types]
identifier은 고유성을 보장하기 위해 (도메인+앱 이름)을 역방향으로 작성한 DNS 문자열을 사용하는 것을 권장합니다.
3. 클라이언트 앱에서 서버 앱의 URL을 호출합니다.
let url = URL(string: "myphotoapp:Vacation?index=1")
UIApplication.shared.open(url!) { (result) in
if result {
// The URL was delivered successfully!
}
}
4. 서버 앱에서 수신한 URL을 처리합니다.
// Scene-based App
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// Determine who sent the URL.
if let urlContext = connectionOptions.urlContexts.first {
let sendingAppID = urlContext.options.sourceApplication
let url = urlContext.url
print("source application = \(sendingAppID ?? "Unknown")")
print("url = \(url)")
// Process the URL.
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let albumPath = components.path,
let params = components.queryItems else {
print("Invalid URL or album path missing")
return false
}
if let photoIndex = params.first(where: { $0.name == "index" })?.value {
print("albumPath = \(albumPath)")
print("photoIndex = \(photoIndex)")
return true
} else {
print("Photo index missing")
return false
}
}
}
앱이 메모리에 실행되고 있지 않은 경우, 시스템은 앱을 실행한 후 scene(_:willConnectTo:options:) 메서드를 호출하여 URL을 전달합니다.
메모리에서 실행 중이거나 정지된 상태일 경우, scene(_:openURLContexts:) 메서드에 URL를 호출하여 전달합니다.
유니버셜 링크 방식
유니버셜 링크를 이용하면 앱이 설치되어 있는 경우 앱으로 이동하고, 없으면 앱을 설치할 수 있는 앱스토어로 이동합니다.
1. 서버 앱에 유니버셜 링크를 지원하기 위해 도메인을 추가합니다.
2. 클라이언트 앱에서 서버 앱의 유니버셜 링크를 호출합니다.
if let appURL = URL(string: "https://myphotoapp.example.com/albums?albumname=vacation&index=1") {
UIApplication.shared.open(appURL) { success in
if success {
print("The URL was delivered successfully.")
} else {
print("The URL failed to open.")
}
}
} else {
print("Invalid URL specified.")
}
3. 서버 앱에서 수신한 유니버셜 링크를 처리합니다.
func scene(_ scene: UIScene, willConnectTo
session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// Get URL components from the incoming user activity.
guard let userActivity = connectionOptions.userActivities.first,
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
return
}
// Check for specific URL components that you need.
guard let path = components.path,
let params = components.queryItems else {
return
}
print("path = \(path)")
if let albumName = params.first(where: { $0.name == "albumname" })?.value,
let photoIndex = params.first(where: { $0.name == "index" })?.value {
print("album = \(albumName)")
print("photoIndex = \(photoIndex)")
} else {
print("Either album name or photo index missing")
}
}