๐ŸŒ™

[Swift] Privacy Masking(Privacy Screen) ๊ตฌํ˜„ํ•˜๊ธฐ

yeggrrr๐Ÿผ 2025. 10. 19. 17:28
728x90

iOS์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋ž˜์—์„œ ์œ„๋กœ ์Šค์™€์ดํ”„๋ž˜ ์•ฑ ์ „ํ™˜๊ธฐ(App Switcher)๋ฅผ ์—ด๋ฉด,
์‹œ์Šคํ…œ์ด ํ˜„์žฌ ์•ฑ์˜ ํ™”๋ฉด ์Šค๋ƒ…์ƒท์„ ์ž๋™์œผ๋กœ ์ฐ์–ด ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์ค€๋‹ค.

๋ฌธ์ œ๋Š” ์ด๋•Œ ๋ฏผ๊ฐ ์ •๋ณด๊ฐ€ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ํ—ฌ์Šค ๋ฐ์ดํ„ฐ, ์‚ฌ์šฉ์ž ํ”„๋กœํ•„, ๋ฉ”์‹œ์ง€ ๋“ฑ์€ ๋ณด์•ˆ์ƒ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋˜๋ฉด ๊ณค๋ž€ํ•˜๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š”,
์ „ํ™˜๊ธฐ๋กœ ์ด๋™ํ•  ๋•Œ ์•ฑ ํ™”๋ฉด ๋Œ€์‹  ๋””ํดํŠธ ๋กœ๊ณ  ํ˜น์€ ์›ํ•˜๋Š” UI๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ํ•ด๋‹น ๋ทฐ๊ฐ€ ๋ณด์ด๋„๋ก ๊ฐ€๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์„ UIKit ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค. (๋งˆ์ง€๋ง‰์— ๊ฐ„๋žตํ•˜๊ฒŒ SwiftUI๋„ ์žˆ์Œ)


 

๋™์ž‘ ์›๋ฆฌ

์•ฑ์ด active → inactive ๋˜๋Š” background ์ƒํƒœ๋กœ ์ „ํ™˜๋˜๋ฉด,
iOS๋Š” ํ˜„์žฌ ์œˆ๋„์šฐ๋ฅผ ๋ Œ๋”๋งํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ž๋™์œผ๋กœ ์บก์ณํ•œ๋‹ค.

์ด ์Šค๋ƒ…์ƒท์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๊ต์ฒดํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•  ์ˆ˜ ์—†๊ณ ,
"๋ฌด์—‡์„ ์ฐํžˆ๊ฒŒ ํ•  ๊ฒƒ์ธ์ง€"๋งŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฆ‰, ์Šค๋ƒ…์ƒท ์ฐ๊ธฐ ์ „์— ์ „์ฒด๋ฅผ ๋ฎ๋Š” ๊ฐ€์งœ ํ™”๋ฉด์„ ์˜ฌ๋ ค๋‘๊ธฐ(์›ํ•˜๋Š” ๋ทฐ)๋ฅผ ํ•˜๋ฉด ๋œ๋‹ค.

 

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

1. ์•ฑ์ด ๋น„ํ™œ์„ฑํ™”๋˜๊ธฐ ์ง์ „(SceneDelegate์˜ sceneWillResignActive) ์‹œ์ ์— ์ „์ฒด๋ฅผ ๋ฎ๋Š” ์ปค๋ฒ„ ์œˆ๋„์šฐ๋ฅผ ์ตœ์ƒ๋‹จ์— ๋„์šฐ๊ธฐ
2. ์ปค๋ฒ„์—๋Š” ๋กœ๊ณ  ํ˜น์€ ์›ํ•˜๋Š” UI๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ
3. ์•ฑ์œผ๋กœ ๋ณต๊ท€(SceneDelegate์˜ sceneDidBecomeActive)ํ•˜์—ฌ ์ปค๋ฒ„๋ฅผ ์ฆ‰์‹œ ์ œ๊ฑฐํ•˜๊ธฐ

์œ„์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜๋ฉด, iOS๊ฐ€ ์บก์ณํ•˜๋Š” ํ™”๋ฉด์€ ํ•ญ์ƒ "๋กœ๊ณ (์›ํ•˜๋Š” UI)"๋กœ ๊ณ ์ •๋œ๋‹ค.

 

๊ตฌํ˜„ ์ฝ”๋“œ
  • ์ปค๋ฒ„ ๋ทฐ ๋งŒ๋“ค๊ธฐ
import UIKit
import SnapKit

final class PrivacyCoverViewController: UIViewController {
	// ์ค€๋น„๋œ ์ž„์‹œ๋กœ๊ณ ๊ฐ€ ์—†๋Š” ๊ด€๊ณ„๋กœ Label๋กœ ํ‘œ์‹œ!
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "yeggrrr"
        label.textAlignment = .center
        label.font = .systemFont(ofSize: 36, weight: .bold)
        label.textColor = .black
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        view.addSubview(titleLabel)
        titleLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
    }

    override var prefersStatusBarHidden: Bool { true } // ์ƒํƒœ๋ฐ” ์ œ๊ฑฐ
}

 

  • ์ปค๋ฒ„๋ทฐ ๋งค๋‹ˆ์ € ๊ตฌ์„ฑ
import UIKit

final class PrivacyCoverManager {
    static let shared = PrivacyCoverManager()
    private init() {}

    private var windows: [ObjectIdentifier: UIWindow] = [:]

    func show(over scene: UIWindowScene) {
        let key = ObjectIdentifier(scene)
        guard windows[key] == nil else { return }

        let w = UIWindow(windowScene: scene)
        w.windowLevel = .alert + 1
        w.rootViewController = PrivacyCoverViewController()
        w.isHidden = false
        w.layoutIfNeeded() // ์Šค๋ƒ…์ƒท ์ง„์ „ ์ฆ‰์‹œ ๋ Œ๋”๋ง
        CATransaction.flush()

        windows[key] = w
    }

    func hide(from scene: UIWindowScene) {
        let key = ObjectIdentifier(scene)
        guard let w = windows.removeValue(forKey: key) else { return }

        UIView.animate(withDuration: 0.12, animations: {
            w.alpha = 0
        }, completion: { _ in
            w.isHidden = true
        })
    }
}

 

  • SceneDelegate์—์„œ์˜ ์ฒ˜๋ฆฌ
import UIKit

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    
	// ์ „ํ™˜๊ธฐ ์ง์ž… ์ง์ „
    func sceneWillResignActive(_ scene: UIScene) {
        guard let ws = scene as? UIWindowScene else { return }
        PrivacyCoverManager.shared.show(over: ws)
    }
	
    // ์•ฑ ๋ณต๊ท€
    func sceneDidBecomeActive(_ scene: UIScene) {
        guard let ws = scene as? UIWindowScene else { return }
        PrivacyCoverManager.shared.hide(from: ws)
    }
}

์œ„์™€ ๊ฐ™์ด ๊ตฌ์„ฑํ•˜๋ฉด ๋!

 


๊ตฌํ˜„๋œ ํ™”๋ฉด

0

1. ์‚ฌ์šฉ์ž ์Šค์™€์ดํ”„ → ์•ฑ inactive ์ „ํ™˜
2. sceneWillResignActive ํ˜ธ์ถœ
3. PrivacyCoverManager.show() → ์ปค๋ฒ„ ํ‘œ์‹œ
4. iOS๊ฐ€ ์Šค๋ƒ…์ƒท ์ดฌ์˜ → ์ปค๋ฒ„๋งŒ ์บก์ณ๋จ
5. ์•ฑ ๋ณต๊ท€ → sceneDidBecomeActive ํ˜ธ์ถœ
6. PrivacyCoverManager.hide() → ์ปค๋ฒ„ ์ œ๊ฑฐ

๊ฒฐ๊ณผ์ ์œผ๋กœ ์œ„์™€ ๊ฐ™์ด ์ „ํ™˜๊ธฐ ์ธ๋„ค์ผ์—๋Š” ํ•ญ์ƒ "yeggrrr" ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๋ทฐ๋งŒ ๋ณด์ด๊ฒŒ ๋œ๋‹ค.


SwiftUI ์—์„œ๋Š”?

swiftUI์—์„œ๋Š” "์Šค๋ƒ…์ƒท/ํ™”๋ฉด ๋…นํ™”/App Switcher ๋ฏธ๋ฆฌ๋ณด๊ธฐ" ์‹œ์ ์—์„œ ํŠน์ • ๋ทฐ๋ฅผ ์ž๋™์œผ๋กœ ๋งˆ์Šคํ‚น ํ•  ์ˆ˜ ์žˆ๋„๋ก
privacySensitive(_:)๋ผ๋Š” modifier๊ฐ€ iOS 15 ์ด์ƒ๋ถ€ํ„ฐ ์ œ๊ณต๋œ๋‹ค.

์ฆ‰, ์œ„์— ์„ค๋ช…ํ•œ UIKit์ฒ˜๋Ÿผ '์ง์ ‘ ์ปค๋ฒ„ ์˜ฌ๋ฆฌ๊ธฐ'๋ฅผ ํ•˜์ง€ ์•Š์•„๋„,
์‹œ์Šคํ…œ์ด ์Šค๋ƒ…์ƒท์„ ์ฐ๋Š” ์ˆœ๊ฐ„์„ ์•Œ์•„์„œ '์ด ๋ทฐ๋Š” ๋ฏผ๊ฐํ•˜๋‹ˆ ๊ฐ€๋ ค์•ผ ํ•จ'์ด๋ผ๊ณ  ์ฒ˜๋ฆฌํ•œ๋‹ค.

<์‚ฌ์šฉ ๋ฐฉ๋ฒ•>

var body: some View {
    MainView()  // ์‹ค์ œ ์ฝ˜ํ…์ธ 
        .privacySensitive() // ← ์ด ํ•œ ์ค„
}

์œ„์™€ ๊ฐ™์ด .privacySensitive() ํ•œ ์ค„๋กœ '๋ธ”๋Ÿฌ/๊ฐ€๋ฆผ' ์ฒ˜๋ฆฌ๊ฐ€ ๋œ๋‹ค.

<์˜ˆ์ œ>

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ ํ™”๋ฉด")
                .font(.largeTitle)
                .padding()

            Text("์ด ํ™”๋ฉด์€ App Switcher์—์„œ ๊ฐ€๋ ค์ง‘๋‹ˆ๋‹ค.")
                .foregroundColor(.secondary)
        }
        .privacySensitive() // ํ•œ ์ค„๋กœ ์ปค๋ฒ„ ๋™์ž‘
    }
}
์ฆ‰, SwiftUI๋Š” ์ด ๊ธฐ๋Šฅ์ด ์–ธ์–ด ๋ ˆ๋ฒจ์—์„œ ๋‚ด์žฅ๋˜์–ด ์žˆ๋Š” ์„ ์–ธ์  ๋ฐฉ์‹์ด๊ณ ,
UIKit์€ ์ง์ ‘ ์œˆ๋„์šฐ๋ฅผ ๋งŒ๋“ค์–ด ๋ฎ๋Š” ๋ช…๋ น์  ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ•œ๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค.

 

728x90