Problem
Yakın zamanda RemafoX için bugüne kadarki en büyük özellik üzerinde çalışmaya karar verdim ve nereden başlayacağımı düşünürken, bir yaşından bile küçük projem için 70’ten fazla target arasında boğulduğumu fark ettim. Uygulamam net kod ayrımı ve daha hızlı build süreleri (= daha hızlı SwiftUI preview’ları, testler ve daha fazlası) için Point-Free’nin bu ücretsiz bölümünde sunduğu vanilla SwiftPM tabanlı yöntemi kullanarak modülerleştiriyorum. Bu uygulama üzerinde yıllarca çalışmayı planladığım için (çevrimiçi olan 27 özellik buzdağının sadece görünen kısmı, çok daha fazla fikrim var), önce bu karmaşayı temizlemeye karar verdim. Sonuçta, özellikler arasında bir tur refactoring yapmak kod tabanını temiz tutar ve geliştiriciyi mutlu eder!
Swift Evolution thread’lerini okurken benim ilgili newsletter’ımdan birinin sayısını hazırlarken @_exported attribute’unu keşfettiğimi hatırladım. Alt çizgili API’ları kullanmak önerilmiyor çünkü davranışları değişebilir veya tamamen kaldırılabilirler, ama çok sayıda düzensiz target’ı temizleme hedefimle alternatifsiz kaldığımı gördüm. Tam da bu nedenle, bu attribute’un tamamen kaldırılma olasılığının nispeten düşük olduğuna kendimi ikna ettim. Herhangi bir şey olursa, ilgili pitch’in bir gün ele alınıp resmi Swift’e gireceğine inanıyorum, böylece @_exported‘ı o zaman adı ne olursa olsun onunla değiştirebiliriz. Ayrıca Point-Free’nin de The Composable Architecture framework’ünde bu attribute’a bağımlı olduğunu keşfettim ve uygulamam zaten bu framework’e ağır şekilde bağımlı. O zaman neden tüm gücümüzle kullanmayalım?
Kısacası, bu attribute’un yardımcı olduğu şey şu: 10 özellik modülün ve 5 yardımcı modülün olduğunu düşün. 10 özelliğin her dosyasında bu 5 yardımcı modülün hepsini (veya çoğunu) import etme eğilimindeyim, bu da şöyle bir şeyle sonuçlanıyor:
import Assets
import Analytics
import ComposableArchitecture
import Constants
import Defaults
import HandySwift
import HelpfulErrorUI
import ReusableUI
import SFSafeSymbols
import SwiftUI
import UtilityVe bu defalarca tekrarlanıyor. Evet, hepsinin bir target’ın her dosyasında gerekmediği doğru, ama gerçek şu ki Xcode zaten modüldeki tek bir dosya import ettiğinde tüm modülü bağlıyor, dolayısıyla hepsini tüm dosyalarda import etmek build sürelerini etkilemez (bildiğim kadarıyla). @_exported import kullanarak tüm bu import’ları, mesela CoreDependencies diye yeni bir target oluşturup içinde şu içerikle bir Swift dosyası oluşturarak birleştirebiliriz:
@_exported import Assets
@_exported import Analytics
@_exported import ComposableArchitecture
@_exported import Constants
@_exported import Defaults
@_exported import HandySwift
@_exported import HelpfulErrorUI
@_exported import ReusableUI
@_exported import SFSafeSymbols
@_exported import SwiftUI
@_exported import UtilityArtık import CoreDependencies yaptığımızda diğer tüm modüller de import edilecek!
Ama her şeyi CoreDependencies diye tek bir gruba koymak gerçekten doğru çözüm mü? Import’ları çok sık tekrarlamak dışındaki bir diğer sorun, şu an bu 70 modülü başka bir gruplama veya yapı olmadığı için alfabetik olarak sıralıyor olmam. Bu gruplama eksikliği sadece kabaca ne aradığımı bilip tam ismini hatırlayamadığımda doğru modülü bulmayı zorlaştırmakla kalmıyor. Aynı zamanda özellikler üzerinde çalışırken ve mümkün olduğunca çok kodu yeniden kullanmaya çalışırken döngüsel bağımlılıklara da yol açabiliyor. Döngüsel bağımlılıkları önlerken kodu yeniden kullanabilmek için neyin nereye ait olduğunun stratejik planlaması gerekiyor.
Çözüm
✨ GÜNCELLEME: Yeni/küçük uygulamalar için aşağıda detaylı anlattığımın basitleştirilmiş bir versiyonunu kullanıyorum. Daha fazlası için FlineDevKit repository’me bak.
Bir soruna pratik çözüm bulmanın en iyi yolu gerçek dünya örneğine bakmaktır. İşte RemafoX’ta gerçekten kullandığım modüllerden bir seçki:
Analytics
Assets
BetterCodable
CommandLineSetup
ComposableArchitecture
Constants
FilesSearch
Foundation
HandySwift
HelpfulErrorUI
MachineTranslation
Paywall
ProjectsBrowser
ReusableUI
SFSafeSymbols
Settings
SwiftUI
UtilityEvet, yukarıdaki listede Foundation ve SwiftUI’ı da listeledim. Neden? Çünkü günün sonunda, diğer import ettiğimiz bağımlılıklar gibi import edilmesi gereken bağımlılıklar, ister harici ister dahili olsun. Onları yerleşik harici bağımlılıklar olarak görüyorum. Her Swift dosyasında en azından import Foundation’a alışkın olabilirsin, ama aslında Foundation olmadan Swift kodu yazabilirsin, sadece Swift Standard Library’de bulunan temel Swift özellikleri olur. Gayet çalışır!
Ve aslında hepimizin bu kadar sık yaptığı bu iki import, Apple’ın kendi iç özellik/yardımcı gruplamasını temsil ediyor: Apple bir sürü işlevselliği Foundation arkasında gruplayıyor ve aynısını SwiftUI veya UIKit/AppKit ile yapıyor. Belirleyici faktör, herhangi bir UI temsil eden veya doğrudan UI ile ilgili olan her şeyin bir gruba, UI temsil etmeyen veya doğrudan UI ile ilgili olmayan her şeyin de diğer gruba ait olması gibi görünüyor. Yapabileceğimiz en doğal şey onların izinden gitmek, hatta Foundation’ı UI olmayan grup ve UI’ı (hem SwiftUI’da hem UIKit’te var olan) UI grubu için kullanarak isimlendirmelerini bile kopyalayabiliriz. Gruplarımız bir uygulamanın alan adına özgü olduğundan, gruplarımızın sonuç isimleri şöyle olurdu: AppFoundation ve AppUI.
Bunu yukarıdaki modül listesine uygulayalım:
// AppFoundation
Analytics
BetterCodable
CommandLineSetup
Constants
FilesSearch
Foundation
HandySwift
MachineTranslation
Utility
// AppUI
Assets
ComposableArchitecture
HelpfulErrorUI
Paywall
ProjectsBrowser
SFSafeSymbols
ReusableUI
Settings
SwiftUIBu zaten daha iyi görünmeye başladı. Ama Apple’ın framework’lerini nasıl yapılandırdığından öğrenebileceğimiz bir şey daha var: Apple her UI olmayan özelliği Foundation’ın parçası olarak bağlamıyor, tüm SwiftUI ile ilgili kodu da SwiftUI’ın parçası olarak göndermiyor. Combine ve Charts ayrı olarak import etmemiz gereken iki framework. Neden onları Foundation ve SwiftUI’ın parçası olarak göndermiyorlar? Çünkü sadece bazı belirli alanlarda faydalılar ve daha genel bir kapsamda gerekmeyebilirler.
İlk problemi hatırlarsan, birçok yerde tekrar tekrar import ettiğim bir modül setim vardı çünkü belirli bir alanda değil, genel olarak faydalı yardımcılardı. Bu yüzden onları birleşik bir grup adıyla import etmek mantıklı. Ama aslında “yardımcı” nedir? Onu daha alana özgü bir özellikten ayıran ne?
Bir özelliği “yardımcı” veya “utility” olarak adlandırıyorum, eğer genel olarak erişilebilir olması geliştirme sürecine zararından çok daha fazla fayda sağlıyorsa. Tabii bu bir ölçüde öznel ama kural olarak şöyle yapıyorum: Özelliği zaten uygulamın birden fazla farklı yerinde kullanıyorsam, artı gelecekte eklemeyi düşünebileceğim 2 veya 3 potansiyel yeni özellik düşünüp en az biri de bundan faydalanabilecekse, muhtemelen genel olarak çok faydalıdır.
Daha pratik açıdan, yukarıdaki modül listesini şöyle ayırırdım:
// (genel olarak faydalı) Yardımcılar
Analytics
Assets
BetterCodable
ComposableArchitecture
Constants
Foundation
HandySwift
HelpfulErrorUI
ReusableUI
SFSafeSymbols
SwiftUI
Utility
// (alana özgü) Özellikler
CommandLineSetup
FilesSearch
MachineTranslation
Paywall
ProjectsBrowser
Settingsİki ayrım boyutunu birleştirirsek, aralarında bağımlılıklar olan 4 çeyrekli şöyle bir grafikle karşılaşırız:

Dikkat edilmesi gereken birkaç önemli nokta:
Yukarıdaki “Özellik” yarısı alttaki “Yardımcılar” yarısı üzerine inşa edilmiştir, dolayısıyla:
Sol üstteki yeşil “UI Olmayan Özellikler” modülleri
import AppFoundationyapabilir.Sağ üstteki kırmızı “UI Özellikleri” modülleri hem
AppFoundationhem deAppUI’ı import edebilir.Bir grup (çeyrek) içinde modüller birbirine bağımlı olabilir (döngüleri önle!).
“UI” modülleri “UI Olmayan” modüllere veya
AppFoundation’a bağımlı olabilir.Alttaki “Yardımcılar” asla yukarıdaki “Özellikler”den import yapmamalı!
Harici modüller de “Özellik” olabilir (sol üstteki gibi, şu an sağ üstte bir tane yok)
Bu yapıyı uygulamak için AppFoundation adında yeni bir modül, artı içinde şu içeriklerle AppFoundation.swift adında yeni bir Swift dosyası oluşturdum:
// System
@_exported import Foundation
// Internal
@_exported import Analytics
@_exported import Constants
@_exported import Utility
// External
@_exported import BetterCodable
@_exported import HandySwiftAyrıca AppUI modülünü de içinde şu içeriklerle AppUI.swift dosyasıyla oluşturdum:
// System
@_exported import SwiftUI
// Internal
@_exported import Assets
@_exported import HelpfulErrorUI
@_exported import ReusableUI
// External
@_exported import ComposableArchitecture
@_exported import SFSafeSymbolsArtık bu yazının başındaki ilk örnekteki, bir UI Özellik modülü içindeki dosyadan aldığım 11 import’u sadece şu 2 satırla değiştirebiliyorum:
import AppFoundation
import AppUI@_exported attribute’u sayesinde 11 import sadece 2’ye düşürüldü.
Foundation veya SwiftUI’ı bile import etmem gerekmediğine dikkat et. Ve herhangi bir UI Olmayan Özellik için tek bir satır yeterli: import AppFoundation!
Tabii bu, başka hiçbir şey import etmeyeceğim anlamına gelmiyor. Dikey eksende hâlâ import’larım olacak; burada belirli bir modül bir grup içinde başka belirli bir modülü import eder, örneğin bir ConfigFile UI özelliği ConfigFileLinter ve ConfigFileNormalizer gibi alt bileşenleri import eder. Ama bunlar alana özgü import’lar ve çok tekrarlayan import’lara yol açmaz.
Son yaptığım şey, ürünlerimi, bağımlılıklarımı ve target’larımı Package.swift dosyamda bu 4 çeyreğe göre gruplamak oldu. Bunun için tüm bölümlere // MARK: - Non-UI Features gibi pragma mark’ları ekledim ve ilgili ifadeleri alfabetik sırayla içlerine koydum. Ortaya çıkan manifest’im artık şuna benziyor:
import PackageDescription
let package = Package(
name: "RemafoX",
platforms: [.macOS(.v12)],
// MARK: - Products
products: [
// MARK: - Grouping Products
.library(name: "AppFoundation", targets: ["AppFoundation"]),
.library(name: "AppUI", targets: ["AppUI"]),
.library(name: "AppTest", targets: ["AppTest"]),
// MARK: - Non-UI Helper Products (AppFoundation)
.library(name: "Analytics", targets: ["Analytics"]),
.library(name: "Constants", targets: ["Constants"]),
.library(name: "Utility", targets: ["Utility"]),
// MARK: - UI Helper Products (AppUI)
.library(name: "Assets", targets: ["Assets"]),
.library(name: "HelpfulErrorUI", targets: ["HelpfulErrorUI"]),
.library(name: "ReusableUI", targets: ["ReusableUI"]),
// MARK: - Test Helper Products (AppTest)
.library(name: "TestResources", targets: ["TestResources"]),
// MARK: - Non-UI Feature Products
.library(name: "CommandLineSetup", targets: ["CommandLineSetup"]),
.library(name: "FilesSearch", targets: ["FilesSearch"]),
.library(name: "MachineTranslation", targets: ["MachineTranslation"]),
// MARK: - UI Feature Products
.library(name: "Paywall", targets: ["Paywall"]),
.library(name: "ProjectsBrowser", targets: ["ProjectsBrowser"]),
.library(name: "Settings", targets: ["Settings"]),
],
// MARK: - Dependencies
dependencies: [
// MARK: - Non-UI Helper Dependencies (AppFoundation)
.package(url: "https://github.com/marksands/BetterCodable.git", from: "0.4.0"),
.package(url: "https://github.com/sindresorhus/Defaults", from: "6.3.0"),
.package(url: "https://github.com/FlineDev/HandySwift", branch: "main"),
// MARK: - UI Helper Dependencies (AppUI)
.package(url: "https://github.com/SFSafeSymbols/SFSafeSymbols", from: "3.3.0"),
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.40.2"),
// MARK: - Test Helper Dependencies (AppTest)
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.3.0"),
// MARK: - Non-UI Feature Dependencies
.package(url: "https://github.com/FlineDev/Microya", branch: "main"),
.package(url: "https://github.com/JohnSundell/Splash.git", from: "0.16.0"),
.package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.3"),
.package(url: "https://github.com/TelemetryDeck/SwiftClient", branch: "main"),
// MARK: - UI Feature Dependencies
],
// MARK: - Targets
targets: [
// MARK: - Grouping Targets
.target(
name: "AppFoundation",
dependencies: [
// Internal
"Analytics",
"Constants",
"Utility",
// External
.product(name: "BetterCodable", package: "BetterCodable"),
.product(name: "HandySwift", package: "HandySwift"),
]
),
.target(
name: "AppUI",
dependencies: [
// Internal
"Assets",
"HelpfulErrorUI",
"ReusableUI",
// External
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
),
.target(
name: "AppTest",
dependencies: [
// Internal
"TestResources",
// External
.product(name: "CustomDump", package: "swift-custom-dump"),
]
),
// MARK: - Non-UI Helper Targets (AppFoundation)
.target(
name: "Analytics",
dependencies: [
// Internal
"Constants",
// External
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "TelemetryClient", package: "SwiftClient"),
]
),
.testTarget(name: "AnalyticsTests", dependencies: ["AppTest", "Analytics"]),
.target(
name: "Constants",
dependencies: [
.product(name: "Defaults", package: "Defaults"),
.product(name: "HandySwift", package: "HandySwift"),
]
),
.target(
name: "Utility",
dependencies: [
// Internal
"Analytics",
"Constants",
// External
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "Defaults", package: "Defaults"),
]
),
.testTarget(name: "UtilityTests", dependencies: ["AppTest", "Utility"]),
// MARK: - UI Helper Targets (AppUI)
.target(
name: "Assets",
dependencies: [.product(name: "Defaults", package: "Defaults")],
resources: [
.process("Colors.xcassets"),
.process("Images.xcassets"),
.copy("Sounds"),
]
),
.target(
name: "HelpfulErrorUI",
dependencies: [
// Internal
"AppFoundation",
"Assets",
"ReusableUI",
]
),
.target(
name: "ReusableUI",
dependencies: [
// Internal
"AppFoundation",
"Assets",
// External
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "Splash", package: "Splash"),
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"),
]
),
// MARK: - Test Helper Targets (AppTest)
.target(
name: "TestResources",
dependencies: [],
path: "TestResources",
exclude: ["Package.swift"],
sources: ["TestResources.swift"],
resources: [
.copy("CustomSample"),
.copy("EmptyFileStructureSamples"),
.copy("GitHubSampleProjects"),
]
),
// MARK: - Non-UI Feature Targets
.target(name: "CommandLineSetup", dependencies: ["AppFoundation"]),
.target(
name: "MachineTranslation",
dependencies: [
// Internal
"AppFoundation",
// External
.product(name: "Microya", package: "Microya"),
]
),
.testTarget(
name: "MachineTranslationTests",
dependencies: ["AppFoundation", "AppTest", "MachineTranslation"],
exclude: ["Resources/secrets.json.sample"],
resources: [.copy("Resources/secrets.json")]
),
// MARK: - UI Feature Targets
.target(name: "Paywall", dependencies: ["AppFoundation", "AppUI"]),
.target(
name: "ProjectSetup",
dependencies: [
"AppFoundation",
"AppUI",
"ProjectDragAndDrop",
"ProjectAnalyzer",
]
),
.target(
name: "Settings",
dependencies: [
"AppFoundation",
"AppUI",
"SettingsTabCurrentPlan",
"SettingsTabGeneral",
"SettingsTabMachineTranslation",
]
),
// ... many more features related to Project, Settings etc.
]
)
AppFoundationveAppUI‘a benzer şekilde, uygulama testlerimdeXCTestveCustomDump(kesinlikle tavsiye ederim!) gibi bağımlılıkları/yardımcıları birleştirmek için kullandığım birAppTestgruplama target’ı da ekledim.
Tüm ilgili import’ları AppFoundation/AppUI ile değiştirmek için şu yöntemi kullandım:
Önce her
@_exportedkütüphanesi için Xcode’un Find & Replace özelliğini kullandım ve tüm import’larıAppFoundationile değiştirdim, böylece birçok dosyada birden fazlaAppFoundationimport’u oldu.Sonra otomatik düzeltme desteği olan SwiftLint’in
duplicate_importskuralını kullandım.brew install swiftlintile kur, sonra şu 3 satırı çalıştır:
echo "only_rules: [duplicate_imports]" > temp_swiftlint.yml
swiftlint lint --config temp_swiftlint.yml --path Sources --autocorrect
rm temp_swiftlint.yml–path’e geçirilen parametreyi, seninkinden farklıysa düzelt.
Son olarak, kendileri
AppFoundation’ın parçası olan modüllerin içindeki dosyalar için değişiklikleri Git kullanarak geri aldım,AppFoundation.swift’in kendisi de dahil.Sonra yukarıdaki adımları
AppUIiçin tekrarladım. Her şey 10 dakikadan az sürdü!
İşte bu kadar! Gördüğün gibi, temizlemeden önce projemde yaklaşık 2.000 import vardı:

Temizlemeden sonra artık sadece 1.200 import var, eskisinden kabaca %40 daha az!

Ayrıca Package.swift manifest dosyam çok kısaldı, 827 satırdan 575 satıra, yani kabaca üçte bir daha az. Ve her şey çok daha düzenli, mutluyum!
Sonuç
@_exported import ve modülleri (A) “UI ile ilgili” veya “UI ile ilgili olmayan” ve (B) daha “genel olarak faydalı” veya daha “alana özgü” olarak sorgulayarak dört gruba ayırma sayesinde, artık sonsuz sayıda “Yardımcı” modülü “Özellik” modüllerime sadece bir veya iki import satırıyla import edebiliyorum! Sadece bu da değil, bu gruplar ve import kuralları aynı zamanda döngüsel bağımlılıkları önlemek için kodumu doğru modüle kolayca yerleştirmem konusunda bir rehber görevi görüyor.
Sonuç: Daha az yazılacak kod, daha az build hatası olasılığı — kazan-kazan!

