SwiftUI preview’larım, Point-Free’nin modülerleştirme yaklaşımını kullanarak Xcode’da Open Focus Timer projesini kurduğum günden beri düzgün çalışmıyordu — model katmanım için iyi bir başlangıç noktası elde etmek amacıyla CoreData checkbox’ını işaretlemiştim. Bu oldukça can sıkıcıydı, çünkü daha hızlı build’ler ve dolayısıyla daha güvenilir SwiftUI preview’ları almak, uygulamam küçük parçalara modülerleştirmek istememin ana nedenlerinden biriydi.
Yayınlarımdan birinde (bu, Twitch’te canlı yayın yaparak tamamen açık bir şekilde geliştirdiğim açık kaynak bir uygulama) bu sorunu bir kez ve tamamen çözmeye karar verdim. Ve başarısız oldum:
Just failed at fixing a #SwiftUI preview error during my #livestream. Could fix one issue, but then got stuck at "MessageError: Connection interrupted". Any ideas? The project is open source:https://t.co/ppevrcRMtK
— Cihat Gündüz (@Jeehut) March 2, 2022
I started getting this error here:https://t.co/AQz2l7vnTv pic.twitter.com/aTYQy5gzHi
Twitter’daki harika Swift topluluğunun yardımıyla sorunun temel nedenini buldum: SwiftUI preview’ları, CoreData modelleri referans verildiğinde sorun çıkarıyor.
Ama bunun basit bir geçici çözümle düzeltilebilecek bir yol sorunu olduğunu düşünürken, o kadar basit değildi. Evet, bir yol sorunu var, ama preview’ları çözerken birden fazla hata seviyesiyle karşılaştım. Ve yol boyunca SwiftUI preview’larını nasıl debug edeceğimi öğrendim. Öğrendiklerimi paylaşayım…
#1: Package Manifest’te Açık Bağımlılıklar
Her şeyden önce. Point-Free’nin modülerleştirme yaklaşımını kullanmak, elle yönetmen gereken bir Package.swift dosyan olacağı anlamına gelir. Her modül için bir target, bir testTarget ve bir library girişi eklersin ve her target için bağımlılıkları belirtmen gerekir. Xcode burada o dosyada yaptığın değişiklikleri tanımaktan başka hiçbir şekilde yardımcı olmuyor. Çok sayıda paket varken manifest dosyası oldukça büyüyebilir ve şu an bunu kolaylaştıracak bir yardım bilmiyorum. İşte şu anda benim manifest’im şöyle görünüyor:
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "OpenFocusTimer",
defaultLocalization: "en",
platforms: [.macOS(.v12), .iOS(.v15)],
products: [
.library(name: "AppEntryPoint", targets: ["AppEntryPoint"]),
.library(name: "Model", targets: ["Model"]),
.library(name: "TimerFeature", targets: ["TimerFeature"]),
.library(name: "ReflectionFeature", targets: ["ReflectionFeature"]),
.library(name: "Resources", targets: ["Resources"]),
],
dependencies: [
// Commonly used data structures for Swift
.package(url: "https://github.com/apple/swift-collections", from: "1.0.2"),
// Handy Swift features that didn't make it into the Swift standard library.
.package(url: "https://github.com/Flinesoft/HandySwift", from: "3.4.0"),
// Handy SwiftUI features that didn't make it into the SwiftUI (yet).
.package(url: "https://github.com/Flinesoft/HandySwiftUI", .branch("main")),
// ⏰ A few schedulers that make working with Combine more testable and more versatile.
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.3"),
// A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.33.1"),
// Safely access Apple's SF Symbols using static typing Topics
.package(url: "https://github.com/SFSafeSymbols/SFSafeSymbols", from: "2.1.3"),
],
targets: [
.target(
name: "AppEntryPoint",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "HandySwiftUI", package: "HandySwiftUI"),
"Model",
"ReflectionFeature",
"TimerFeature",
"Utility",
]
),
.target(
name: "Model",
dependencies: [
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"),
],
resources: [
.process("Model.xcdatamodeld")
]
),
.target(
name: "TimerFeature",
dependencies: [
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
"Model",
"ReflectionFeature",
"Resources",
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"),
"Utility",
]
),
.target(
name: "ReflectionFeature",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "HandySwift", package: "HandySwift"),
"Model",
"Resources",
"Utility",
]
),
.target(
name: "Resources",
resources: [
.process("Localizable")
]
),
.target(
name: "Utility",
dependencies: [
.product(name: "CombineSchedulers", package: "combine-schedulers"),
"Model",
]
),
.testTarget(
name: "ModelTests",
dependencies: ["Model"]
),
.testTarget(
name: "TimerFeatureTests",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
"TimerFeature",
]
),
]
)Bu dosyayı elle yönetmenin sorunu sadece manuel iş değil. Xcode bağımlılıklar konusunda tutarsız davranıyor gibi görünüyor: Örneğin Simülatörü hedefleyen normal bir build yaptığında, bir bağımlılığın bağımlılığı otomatik olarak target’ına bağlanıyor gibi görünüyor. Yani TimerFeature’ım Utility‘yi import ediyorsa ama bu TimerFeature target’ının altında bağımlılık olarak listelenmemişse, Xcode yine de hatasız derleyebilir çünkü başka bir bağımlılık, örneğin Model de Utility’ye bağımlıdır ve Xcode dolaylı olarak TimerFeature içinde Utility’ye erişebilir.
Bu kulağa çok faydalı gelse de oldukça sinir bozucu olabiliyor çünkü SwiftUI preview’ları farklı çalışıyor. Görebildiğim kadarıyla, preview’larda bu geçişli tür dolaylı import’lar çalışmıyor. Aynı şey testleri çalıştırırken de geçerli gibi görünüyor (en azından bazen). Başka bir deyişle: Her target için dependencies‘i her zaman iki kez kontrol etmek ve bir target’ta yaptığın her import‘u Package.swift manifest dosyanızdaki ilgili target’a eklemeyi unutmamak önemli.
Belki gelecekte birisi bunu kolaylaştıracak bir araç yazar.
#2: Üretilen Kodun Xcode Tarafından Güvenilir Şekilde Algılanmaması
Karşılaştığım bir diğer sorun, build’lerim başarılı olsa bile, Xcode’un (“Build succeeded” dialogunu gösterdikten sonra) PreviewProvider içindeki editörde FocusTimer’ı bulamadığını belirten bir hata göstermesiydi:

Bu üretilmiş dosyaları modelde her değişiklik yaptığında silip yeniden oluşturman gerekecek (veritabanı migrasyon sorunlarını önlemek için bunu nadiren yapman gerekir zaten). Ek olarak, Xcode’da modeli seç ve Codegen’i Manual/None olarak ayarla.

Bunu yaptıktan sonra editör artık hata göstermiyor.
#3: SwiftUI Diagnostics != SwiftUI Crash Reports
İşte SwiftUI preview’ları başarısız olduğunda Diagnostics butonuna bastıktan sonra böyle hatalardan nasıl faydalanılacağına dair (benim gibi) merak edenler için bir öğrenim:

Vurgulanan klasörün içeriğini görüntülemek, SwiftUI preview build’i hakkında farklı türde ayrıntılar tutan birçok dosyayı ortaya çıkaracaktır. Debug için en faydalı dosya CrashLogs klasörünün içindedir; burada Xcode’da çift tıklayarak kolayca açabileceğimiz bir veya birden fazla .ips dosyası bulabilirsin:

Neyse ki burada Twitter’daki Swift topluluğundan nazik bir geliştiricinin işareti yardımcı oldu ve beni StackOverflow’daki bu cevaba yönlendirdi.
Temel olarak Xcode’da (veya SwiftPM’de?) şu an bir bug olduğunu ve Bundle.module‘ün SwiftUI preview’larında yanlış yolu gösterdiğini söylüyor. Bunu düzeltmek için özel bir aramayla bir Bundle extension’ı eklemeyi öneriyorlar. İşte benim kodlama ve yorum tarzıma uyacak şekilde biraz düzenlenmiş tam kod:
import Foundation
extension Foundation.Bundle {
/// Workaround for making `Bundle.module` work in SwiftUI previews. See: https://stackoverflow.com/a/65789298
///
/// - Returns: The bundle of the target with a path that works in SwiftUI previews, too.
static var swiftUIPreviewsCompatibleModule: Bundle {
#if DEBUG
// adjust these for each module
let packageName = "OpenFocusTimer"
let targetName = "Model"
final class ModuleToken {}
let candidateUrls: [URL?] = [
// Bundle should be present here when the package is linked into an App.
Bundle.main.resourceURL,
// Bundle should be present here when the package is linked into a framework.
Bundle(for: ModuleToken.self).resourceURL,
// For command-line tools.
Bundle.main.bundleURL,
// Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/").
Bundle(for: ModuleToken.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent()
.deletingLastPathComponent(),
Bundle(for: ModuleToken.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
]
// The name of your local package, prepended by "LocalPackages_" for iOS and "PackageName_" for macOS.
let bundleNameCandidates = ["\(packageName)_\(targetName)", "LocalPackages_\(targetName)"]
for bundleNameCandidate in bundleNameCandidates {
for candidateUrl in candidateUrls where candidateUrl != nil {
let bundlePath: URL = candidateUrl!.appendingPathComponent(bundleNameCandidate)
.appendingPathExtension("bundle")
if let bundle = Bundle(url: bundlePath) { return bundle }
}
}
return Bundle.module
#else
return Bundle.module
#endif
}
}Bu kodu kopyalayıp yapıştırırken
packageNamevetargetNamedeğişkenlerini kendi paket ve target adlarına göre düzenlemeyi unutma.
Geçici çözümü #if DEBUG içine sardığıma dikkat et, böylece production kodum yanlışlıkla bu yol aramasını kullanmaz ve resmi Bundle.module‘e güvenir. Ayrıca StackOverflow’da bulunan geçici çözüm kodundaki fatalError’ı kaldırdım, böylece özel arama yollarında bir Bundle bulamazsa çökmek yerine Bundle.module’ü fallback olarak return ediyorum. Bu, kodu daha dayanıklı yapmayı ve gelecekteki bir Xcode sürümünde bu bug düzeltilse bile özel arama yolları artık çalışmasa bile çalışmaya devam etmesini amaçlıyor.
Şimdi PersistenceController’da yapmam gereken son değişiklik, Bundle.module çağrısını yeni Bundle.swiftUIPreviewsCompatibleModule çağrısıyla değiştirmekti:
let modelUrl = Bundle.swiftUIPreviewsCompatibleModule.url(forResource: "Model", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelUrl)!
container = NSPersistentContainer(name: "Model", managedObjectModel: managedObjectModel)Ve sonunda SwiftUI preview’larım tekrar çalışmaya başladı!

