์คํ๋์?
์ฑ์ ์ฒ์ ์คํํ์ ๋ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ง๋ ์ฒซ ํ๋ฉด์ '์คํ๋์ ํ๋ฉด(Splash Screen)์ด๋ผ๊ณ ํ๋ค.
๋จ์ํ ๋ธ๋๋ ๋ก๊ณ ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒฝ์ฐ๋ ์๊ณ ,
์ด๊ธฐํ ์์
์ด๋ ์๊ฒฉ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ ์ ์ ์ ๋
ธ์ถ๋๋ ์ฉ๋๋ก ํ์ฉ๋๊ธฐ๋ ํ๋ค.
๊ทธ๋ฌ๋ ๊ตฌํ ๋ฐฉ์์ ๋ฐ๋ผ UX์ ์ฌ์ฌ ๊ธฐ์ค์ ์ฐจ์ด๊ฐ ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ ์ฌ๋ฐ๋ฅธ ๊ตฌํ๋ฒ์ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค.
โท LaunchScreen
- ์์คํ
์์ ์ ๊ณตํ๋ ์ด๊ธฐ ํ๋ฉด
- ์ฑ ์คํ ์งํ ๊ฐ์ฅ ๋จผ์ ํ์๋๋ฉฐ, Xcode์ LaunchScreen.storyboard๋ฅผ ํตํด ๊ตฌ์ฑ
- ์ฅ์ : ๊น๋ํ๊ณ ์คํ ์งํ ํ์, iOS ์ฌ์ฌ ๊ฐ์ด๋๋ผ์ธ์ ๋ถํฉ
- ๋จ์ : ๋์ ๋ก์ง ์ ์ฉ ๋ถ๊ฐ → ๋คํธ์ํฌ ์ด๋ฏธ์ง ๋ก๋ฉ, ์ ๋๋ฉ์ด์
์ฝ์
๋ฑ์ด ๋ถ๊ฐ๋ฅํจ
โท Custom SplashView
- ์ฑ ์คํ ํ, rootViewController์์ ์ง์ ๋์ฐ๋ ViewController
- ์ฅ์ : ๋์ ์ด๋ฏธ์ง ๊ต์ฒด, ์ ๋๋ฉ์ด์
, API ์ฐ๋ ๊ฐ๋ฅ
- ๋จ์ : ์ฑ ์คํ ์๋์ ๋ฐ๋ผ ํ๋ฉด ์ ํ ์ง์ฐ ๊ฐ๋ฅ์ฑ ์์
์ฆ, ๊ณ ์ ์ ์ธ ์คํ๋์ฌ ํ๋ฉด์ ๊ตฌํํด์ผํ๋ค๋ฉด? LaunchScreen ์ฌ์ฉ
์๋ฒ๋ก ๋ถํฐ ๋ฐ์ ์ด๋ฏธ์ง ๋ฑ ๋์ ์ธ ์คํ๋์ ํ๋ฉด์ ๊ตฌํํด์ผํ๋ค๋ฉด? ์ปค์คํ ๋ทฐ ๊ตฌ์ฑ
์ด๋ฒ ๊ธ์๋ 'API ๊ธฐ๋ฐ ๋์ ์ด๋ฏธ์ง ๋ก๋ฉ ๊ตฌํ ๋ฐฉ๋ฒ'์ ๋ํด์ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค.
๊ตฌํํ๋ ค๋ ๋์์ ์ฑ ๋ธ๋๋ ์ด๋ฏธ์ง๋ฅผ Default๋ก ์ ์ฅํด๋๊ณ ,
์๋ฒ์์ ์ด๋ฏธ์ง๋ฅผ ๋ณด๋ด์ฃผ๋ฉด ํด๋น ์ด๋ฏธ์ง๋ฅผ ๋ค์ ์คํ๋๋ถํฐ ๋
ธ์ถํ๋ ค๊ณ ํ๋ค. (๋ง์ฝ ์๋ค๋ฉด, ๋ค์ Default๋ก)
ํ๋ฆ์ ์๋์ ๊ฐ๋ค.
โ ์ฑ ์คํ → LaunchScreen ๋
ธ์ถ(์ด๋ ๋ฐฐ๊ฒฝ์์ ์ฑ ๋ธ๋๋ ์ปฌ๋ฌ๋ก ์ง์ ํ๋ ๊ฒ์ ์ถ์ฒ)
โก ์ปค์คํ
SplashView ์ ํ ํ Default ์ด๋ฏธ์ง ํ์ → API ํธ์ถ → ๋ค์ ๋ทฐ์ ๋ณด์ฌ์ค ์ด๋ฏธ์ง ์ ์ฅ
โข Duration ๋งํผ ๋ณด์ฌ์ค ํ, ๋ฉ์ธ ๋ทฐ(MainViewController)๋ก ์ ํ
<๊ตฌํ ์ ๊ณ ๋ ค์ฌํญ>
- ์ฌ์ฌ ๊ฐ์ด๋๋ผ์ธ: LaunchScreen์ ๊ด๊ณ ์ฑ ์ด๋ฏธ์ง ์ฝ์ ๋ถ๊ฐ. ๋จ์ ๋ธ๋๋/์ฑ ์ ์ฒด์ฑ์ ๋ํ๋ด์ผํจ. (๊ทธ๋ฅ ๋น ํ๋ฉด์ผ๋ก ๋ฌ๋ ๋ฌด๊ดํจ)
- ์ฑ๋ฅ ์ต์ ํ: API ํธ์ถ ๋ฐ ์ด๋ฏธ์ง ๋ค์ด๋ก๋๋ ๋น๋๊ธฐ ์ฒ๋ฆฌ, ๋ฉ์ธ ์ค๋ ๋ ๋ธ๋กํน ๊ธ์ง.
- Fallback ์ ๋ต: API ์คํจ ์์๋ ์์ฐ์ค๋ฝ๊ฒ ์ฑ์ด ์คํ๋๋๋ก ๊ธฐ๋ณธ ์ด๋ฏธ์ง๋ฅผ ํญ์ ์ค๋นํด์ผํจ.
- ๋ฐฑ์ ์ ์ฑ : Application Support์ ์ ์ฅํ ์บ ํ์ธ์ฑ ์ด๋ฏธ์ง๋ iCloud ๋ฐฑ์ ์ ์ธ(isExcludedFromBackup)
๋ก ์ค์ ํด ์ฉ๋ ๋ฐ ์ฌ์ฌ ๋ฆฌ์คํฌ๋ฅผ ์ค์ฌ์ผํจ
- ์ฉ๋ ์ ํ: ๋น์ ์ ๋์ฉ๋ ๋ฆฌ์์ค ์ ์ ์ ๋ง๊ธฐ ์ํด '์ด๋ฏธ์ง ์ต๋ ์ฉ๋'์ ์ด๊ณผํ๋ฉด ํ๊ธฐํ๋๋ก ๊ตฌํํ๋ ๊ฒ์ ๊ถ์ฅํจ.
์๋ ์์ ์ฝ๋๋ '์๋ฒ ์ด๋ฏธ์ง๋ ์ ์ฅํด๋์๋ค๊ฐ ๋ค์ ์คํ๋ถํฐ ๋ณด์ฌ์ฃผ๊ณ , ์คํจ ๋๋ ์ด๋ฏธ์ง๊ฐ ์๋ค๋ฉด ๊ธฐ๋ณธ ์ด๋ฏธ์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ' ๊ตฌ์กฐ์ด๋ค.
import UIKit
// ์คํ๋์ ์ด๋ฏธ์ง ํ์ผ๊ณผ ๋ฉํ๋ฐ์ดํฐ ๊ด๋ฆฌ
enum SplashAssetStore {
private static var baseDir: URL = {
let dir = try! FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("splash", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}()
private static let fileName = "splash.jpg"
private static let urlKey = "splash.lastRemoteURL"
private static let dateKey = "splash.savedAt"
static func loadCachedImage() -> UIImage? {
let path = baseDir.appendingPathComponent(fileName)
guard let data = try? Data(contentsOf: path) else { return nil }
return UIImage(data: data)
}
static func save(imageData: Data, remoteURL: URL) throws {
let path = baseDir.appendingPathComponent(fileName)
try imageData.write(to: path, options: .atomic)
UserDefaults.standard.set(remoteURL.absoluteString, forKey: urlKey)
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: dateKey)
}
static func savedRemoteURL() -> URL? {
guard let s = UserDefaults.standard.string(forKey: urlKey) else { return nil }
return URL(string: s)
}
static func clear() {
let path = baseDir.appendingPathComponent(fileName)
try? FileManager.default.removeItem(at: path)
UserDefaults.standard.removeObject(forKey: urlKey)
UserDefaults.standard.removeObject(forKey: dateKey)
}
}
- Application Support์ splash.jpg๋ฅผ ์ ์ฅ
- ํ์ผ์ ๋ค์ ์คํ ๋ ๋ก๋
- UserDefaults์๋ ์ด๋ค URL์์ ๋ด๋ ค์จ ์ด๋ฏธ์ง์ธ์ง, ์ธ์ ์ ์ฅํ๋์ง ๊ธฐ๋ก
- ์ฑ ์ญ์ ํ์๋ ๋จ๊ธธ ํ์๊ฐ ์๋ค๋ฉด Caches ๋๋ ํฐ๋ฆฌ
- ์ข ๋ ์์ ์ ์ผ๋ก ์ ์งํ๋ ค๋ฉด Application Support ๊ถ์ฅ โ
Caches ๋๋ ํฐ๋ฆฌ
- ํน์ง: ์์คํ ์ด ํ์ํ ๋ ์๋์ผ๋ก ์ญ์ ํ ์ ์๋ ๊ณต๊ฐ
- ์ฌ์ฉ ๋ชฉ์ : ๋ค์ ๋ฐ์์ฌ ์ ์๋ ๋ฐ์ดํฐ(ex. ์ด๋ฏธ์ง ์บ์, API ์๋ต ์บ์)
- ์ฅ์ : ๋์คํฌ ๊ณต๊ฐ์ด ๋ถ์กฑํ๋ฉด iOS๊ฐ ์์์ ์ ๋ฆฌํด์ฃผ๋ฏ๋ก ๊ด๋ฆฌ ๋ถ๋ด์ด ์ ์
- ๋จ์ : ์ฑ ์ญ์ ์ ์๋ ์ฌ๋ผ์ง ์ ์๊ธฐ ๋๋ฌธ์ '๊ผญ ์ ์งํด์ผ ํ๋ ๋ฐ์ดํฐ'๋ฅผ ์ ์ฅํ๊ธฐ์๋ ์ ํฉํ์ง ์์
Application Support ๋๋ ํฐ๋ฆฌ
- ํน์ง: ์ฑ์ด ์ญ์ ๋๊ธฐ ์ ๊น์ง๋ OS๊ฐ ์์๋ก ์ญ์ ํ์ง ์์.
- ์ฌ์ฉ ๋ชฉ์ : ์ฑ ์คํ์ ๊ผญ ํ์ํ ๋ฐ์ดํฐ, ์์ ์ ์ผ๋ก ๋ณด๊ดํด์ผ ํ๋ ์ค์ ํ์ผ์ด๋ ๋ฆฌ์์ค
- ์ฅ์ : ๊ฐ๋ฐ์๊ฐ ์ง์ ์ง์ฐ์ง ์๋ ์ด์ ์ฑ์ด ์ด์์๋ ๋์์ ๋ณด์กด๋จ
- ๋จ์ : ๋์คํฌ ๊ณต๊ฐ์ ์ฐจ์งํ๋ฏ๋ก ๋ถํ์ํ ๋ฐ์ดํฐ๋ ์ง์ ์ ๋ฆฌํด์ค์ผํจ
๊ฐ์ธ์ ์ผ๋ก ๊ตฌํํ๊ณ ์ ํ๋ ๊ตฌ์กฐ๋ ํ์! (์ฑ ์ญ์ ์ ๊น์ง๋ ์ ์ฅ๋๋๋ก ํ๊ณ ์ถ์.)
- ์ด๋ฏธ์ง ํ์ผ์ Application Support์ ์ ์ฅ
- ๋ฉํ๋ฐ์ดํฐ(์ด๋ฏธ์ง URL, ์ ์ฅ ์๊ฐ, ๋
ธ์ถ Duration ๋ฑ)์ UserDefaults์ ์ ์ฅ
import Foundation
import UIKit
struct SplashPayload: Decodable {
let imageUrl: URL?
}
// ์๋ฒ์์ ์คํ๋์ ์ด๋ฏธ์ง URL ๋ฐ์์ค๊ธฐ + ์ค์ ์ด๋ฏธ์ง ๋ค์ด๋ก๋
enum SplashService {
static func fetchPayload() async throws -> SplashPayload {
let url = URL(string: "https://yegr.com/api/splash")!
var req = URLRequest(url: url)
req.timeoutInterval = 5
let (data, resp) = try await URLSession.shared.data(for: req)
guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(SplashPayload.self, from: data)
}
static func downloadImageData(from url: URL) async throws -> Data {
let (data, resp) = try await URLSession.shared.data(from: url)
guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
guard UIImage(data: data) != nil else {
throw URLError(.cannotDecodeRawData)
}
return data
}
}
- fetchPayload()๋ฅผ ํตํด ์๋ฒ์์ imageUrl์ ๋ฐ์์ด
- downloadImageData(from:)์ ์ค์ ์ด๋ฏธ์ง๋ฅผ ๋ด๋ ค๋ฐ๊ณ ์ ํจ์ฑ ๊ฒ์ฆ๊น์ง ์ํํจ
import UIKit
import SnapKit
final class SplashViewController: UIViewController {
private let imageView = UIImageView()
private let defaultImage: UIImage
private let minimumDisplaySeconds: TimeInterval
init(defaultImage: UIImage, minimumDisplaySeconds: TimeInterval = 2.0) {
self.defaultImage = defaultImage
self.minimumDisplaySeconds = minimumDisplaySeconds
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yegrBrandColor // ๋ธ๋๋ ์ปฌ๋ฌ๋ก ์ง์ !
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
imageView.snp.makeConstraints {
$0.center.equalToSuperview()
$0.width.lessThanOrEqualTo(view.snp.width).multipliedBy(0.6)
$0.height.lessThanOrEqualTo(view.snp.height).multipliedBy(0.6)
}
imageView.image = SplashAssetStore.loadCachedImage() ?? defaultImage
Task { await prefetchNextRunImage() }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Task { @MainActor in
try? await Task.sleep(nanoseconds: UInt64(minimumDisplaySeconds * 1_000_000_000))
transitionToMain()
}
}
@MainActor
private func transitionToMain() {
let main = MainViewController()
let window = view.window ?? UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.first
window?.rootViewController = main
window?.makeKeyAndVisible()
let t = CATransition()
t.type = .fade
t.duration = 0.25
window?.layer.add(t, forKey: kCATransition)
}
// ์๋ฒ์์ ์ ์ด๋ฏธ์ง๋ฅผ ๋ฐ์ผ๋ฉด ํ์ผ๋ก ์ ์ฅ
private func prefetchNextRunImage() async {
do {
let payload = try await SplashService.fetchPayload()
guard let url = payload.imageUrl else { return } // ์ด๋ฏธ์ง ์์ → ํ์ ์ ์ง
if SplashAssetStore.savedRemoteURL() == url { return }
let data = try await SplashService.downloadImageData(from: url)
try SplashAssetStore.save(imageData: data, remoteURL: url) // ๋ค์ ์คํ๋ถํฐ ์ฌ์ฉ
} catch {
print("์ด๋ฏธ์ง ๋ฐ๊ธฐ ์คํจ!(์คํ์ ์ํฅ์์ ๊ธฐ๋ณธ/์บ์ ์ด๋ฏธ์ง๋ก ํ์๋จ)")
}
}
}
- ์คํ → ์ฆ์ ์บ์ ์ด๋ฏธ์ง ํน์ Default ์ด๋ฏธ์ง ํ์
- ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์๋ฒ ํธ์ถ → ์ด๋ฏธ์ง ๋ค์ด๋ก๋ ์ฑ๊ณต ์ ํ์ผ ์ ์ฅ
- ์ ์ฅ๋ ์ด๋ฏธ์ง๋ ๋ค์ ์คํ ์ ์ฆ์ ํ์
- ์คํจ/์ด๋ฏธ์ง ์์์ด๋ฉด ๊ทธ๋ฅ ๋์ด๊ฐ๋๋ก ๊ตฌํ
import UIKit
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let ws = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: ws)
// ์์
์ ๋ฃ์ด๋ ๋ธ๋๋ ๊ธฐ๋ณธ ์ด๋ฏธ์ง ์ฌ์ฉ
let splashVC = SplashViewController(defaultImage: UIImage(named: "splash_default")!,
minimumDisplaySeconds: 2.0)
window.rootViewController = splashVC
window.makeKeyAndVisible()
self.window = window
}
}
์์ ๊ฐ์ด ๊ตฌํํจ์ผ๋ก์, LaunchScreen๊ณผ Custom SplashView์ ์ฐจ์ด๋ฅผ ์ ๋ฆฌํ๊ณ ,
API ๊ธฐ๋ฐ ๋์ ์คํ๋์ ํ๋ฉด์ ์ด๋ป๊ฒ ์ค๊ณํ๋ฉด ์ข์์ง ๊ณ ๋ฏผํ ์ ์์๋ค.
(์ฌ์ค ํ์ฌ์์ ์์ฒญํ๋ ์ฌํญ์ด๋ผ ๊ณ ๋ฏผํ๊ฒ๋จ.
๋จ์ํ ๊ทธ๋ฅ ์ ์ฅํ๊ณ ๋ค์ ์คํ์ ์๋ก์ด ์ด๋ฏธ์ง ๋ณด์ฌ์ฃผ๋ฉด ๋๊ฒ ์ง~ ์ ๋๋ก๋ง ์๊ฐํ๋ค๊ฐ,
๋ง์ ๊ตฌ์กฐ๋ฅผ ์ง๋ค๋ณด๋ ๋จ์ํ์ง ์์ ๋ถ๋ถ์ด ๋ง๋ค๋ ๊ฒ์ ๊นจ๋ฌ์..
๊ทธ๋์ API ๋ง๋ค์ด์ง๊ธฐ ์ ์ ๋ฏธ๋ฆฌ ์ ๋ฆฌํด๋๋ฉด ์ข๊ฒ ๋ค๊ณ ์๊ฐํ๊ณ ,
์ด๋ฒ ๊ธ์ ๋ธ๋ก๊ทธ ์ฃผ์ ๋ก ์ผ๊ฒ ๋์๋ค๋..๐ฅน)
๋!!!!
'๐' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Swift] KeychainManager (0) | 2025.09.21 |
---|---|
[Swift] Keychain (0) | 2025.09.14 |