Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/developeracademy-postech/2024-nc2-m20-screentime


https://github.com/developeracademy-postech/2024-nc2-m20-screentime

Last synced: about 13 hours ago
JSON representation

Awesome Lists containing this project

README

        

# 2024-NC2-M20-ScreenTime


2024 Apple Deveopler Academy @ POSTECH, Morning Session AengZi & TEO
๐Ÿ“… 2024.06.10(Mon) - 2024.06.21(Fri)
๐Ÿ“ฒ ios 17.4/SwiftUI/

## ๐ŸŽฅ Youtube Link
(์ถ”ํ›„ ๋งŒ๋“ค์–ด์ง„ ์œ ํŠœ๋ธŒ ๋งํฌ ์ถ”๊ฐ€)

## โณ About Screen Time
![ScreenTime](https://github.com/DeveloperAcademy-POSTECH/2024-NC2-M20-ScreenTime/blob/main/ScreenTime.png)

> ๋””์ง€ํ„ธ ๊ธฐ๊ธฐ์˜ ์‚ฌ์šฉ ์‹œ๊ฐ„์„ ์ถ”์ ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ
>

**Screen Time**์€ ์›น ์‚ฌ์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ  ๋ฐ ์ถ”์ , ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ๊ณผ ๊ด€๋ จ๋œ ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค.

$\color{#5c56d2} \bf ๊ธฐ๊ธฐ$ $\color{#5c56d2} \bf ์‚ฌ์šฉ์‹œ๊ฐ„$ $\color{#5c56d2} \bf ์ถ”์ $ DeviceActivity

๊ธฐ๊ธฐ ์‚ฌ์šฉ์‹œ๊ฐ„, ์•ฑ ์„œ๋น„์Šค, ์›น ์‚ฌ์ดํŠธ์— ๋จธ๋ฌธ ์‹œ๊ฐ„์„ ์ถ”์ ํ•˜๊ณ  ๊ธฐ๋ก, ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

$\color{#5c56d2} \bf ์Šคํฌ๋ฆฐํƒ€์ž„$ $\color{#5c56d2} \bf ์ œ์–ด$ Managed Settings

๊ธฐ๊ธฐ ์‚ฌ์šฉ ์‹œ๊ฐ„์„ ์ถ”์ ํ•ด์„œ ํŠน์ • ์–ดํ”Œ์ด๋‚˜ ์›น ์‚ฌ์ดํŠธ์— ๋จธ๋ฌด๋ฅด๋Š” ์‹œ๊ฐ„์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

$\color{#5c56d2} \bf ์Šคํฌ๋ฆฐํƒ€์ž„$ $\color{#5c56d2} \bf ์‚ฌ์šฉ๋Ÿ‰$ $\color{#5c56d2} \bf ๊ณต์œ $ FamilyControls

๋ณดํ˜ธ์ž๊ฐ€ ์ž๋…€์˜ ์Šคํฌ๋ฆฐ ํƒ€์ž„์„ ๊ณต์œ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ณ  ๊ทธ๊ฒƒ์„ ๋ณดํ˜ธ์ฐจ์›์—์„œ ์ œํ•œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

## ๐ŸŽฏ What we focus on?
### **๐Ÿ”ย ์šฐ๋ฆฌ๊ฐ€ Screen Time์—์„œ ์ง‘์ค‘ํ•œ โ€˜์Šคํฌ๋ฆฐํƒ€์ž„ ์ œ์–ดโ€™**(Managed Settings)

**1. ๋‹ค์šดํƒ€์ž„**

ํŠน์ • ์‹œ๊ฐ„์„ ์ •ํ•ด์„œ ๊ทธ ์‹œ๊ฐ„๋Œ€์—๋Š” ๊ธฐ๋ณธ์ ์ธ ์•ฑ์ธ ์„ค์ •, ๊ฑด๊ฐ•, ์ „ํ™” ๋“ฑ์˜ ์•ฑ๊ณผ ๋ณ„๊ฐœ๋กœ ์„ ํƒํ•ด ๋‘” ์•ฑ ์ด์™ธ์˜ ์‚ฌ์šฉ์„ ์ œํ•œ

**2. ์•ฑ ์‹œ๊ฐ„ ์ œํ•œ**

ํŠน์ • ์•ฑ์„ ์ผ์ •์‹œ๊ฐ„ ๋™์•ˆ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ œํ•œ

**3. ์ปจํ…์ธ  ๋ฐ ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ์ œํ•œ**

์•ฑ ์„ค์น˜์™€ ์‚ญ์ œ, ํŠน์ • ์‚ฌ์ดํŠธ์™€ ์ฝ˜ํ…์ธ ๋ฅผ ์•„์˜ˆ ์ ‘์†ํ•  ์ˆ˜ ์—†๋„๋ก ์„ค์ •ํ•˜๋Š” ๊ธฐ๋Šฅ

## ๐Ÿ’ผ Use Case
- ์ง‘์ค‘๋ชจ๋“œ(Focus)์—์„œ๋Š” ์œ ์ €์™€ ์œ ์ €์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์•ฑ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ ์ œํ•œ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์ง€๋งŒ ์‹œ๊ฐ„ ์„ค์ •์ด ์–ด๋ ต๋‹ค!
- ์Šคํฌ๋ฆฐํƒ€์ž„(Screen Time)์—์„œ๋Š” ์•ฑ ์ œํ•œ ๋ฐ ์‹œ๊ฐ„ ์„ค์ •์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์œ ์ €์™€ ์œ ์ €์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์—†๋‹ค!

โ†’ Screen Time์˜ ์•ฑ ์ ‘๊ทผ/์‚ฌ์šฉ์„ ๊ด€๋ฆฌํ•˜๋Š” ManagedSettings ํ”„๋ ˆ์ž„์›Œํฌ: ๋‹ค์šดํƒ€์ž„ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋งž๋Š” ๋‹ค์šดํƒ€์ž„ ์ง‘์ค‘๋ชจ๋“œ๋ฅผ ๊ฐœ๋ฐœํ•ด๋ณด์ž!

### โ†’ ์•ฑ ์‚ฌ์šฉ ์‹œ๊ฐ„์„ ์ œํ•œํ•จ์œผ๋กœ ์—…๋ฌด์— ๋Œ€ํ•œ ์ง‘์ค‘์„ ๋„์™€์ฃผ๋Š” $\color{#5c56d2} \bf ๋ฝ€๋ชจ๋„๋กœ$ $\color{#5c56d2} \bf ํƒ€์ด๋จธ$ ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž!
> ๋ฝ€๋ชจ๋„๋กœ ๊ธฐ๋ฒ•(Pomodoro Technique)์ด๋ž€? ์ง‘์ค‘๋ ฅ ํ–ฅ์ƒ์ด ๋ชฉ์ ์ธ ์‹œ๊ฐ„ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•๋ก ์œผ๋กœ, **25๋ถ„ ๋™์•ˆ ๋ฌด์–ธ๊ฐ€์— ์ง‘์ค‘ํ•˜๊ณ  5๋ถ„ ๋™์•ˆ ์‰ฌ๋Š” ๊ฒƒ์„ 4๋ฒˆ ๋ฐ˜๋ณตํ•˜๊ณ , ๊ทธ ๋’ค์— 30๋ถ„๊ฐ„ ์‰ฌ๋„๋ก ์‹œ๊ฐ„์„ ๋ฐฐ๋ถ„**ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค. ์ด ๋•Œ, "25๋ถ„ ์ง‘์ค‘ + 5๋ถ„ ํœด์‹"์˜ ์‚ฌ์ดํด์„ ๊ฐ€๋ฆฌ์ผœ โ€œ๋ฝ€๋ชจ๋„๋กœโ€๋ผ๊ณ  ํ•˜๊ณ , ์ด ์‚ฌ์ดํด์„ 1ํšŒ ์™„๋ฃŒํ•˜๋Š” ๊ฒƒ์„ "1 ๋ฝ€๋ชจ๋„๋กœ" ๋ผ๊ณ  ํ•œ๋‹ค.

## ๐Ÿ–ผ๏ธ Prototype
![prototype](https://github.com/DeveloperAcademy-POSTECH/2024-NC2-M20-ScreenTime/blob/main/prototype.gif)
* ์‹คํ–‰ ๊ณผ์ •์„ ๋น ๋ฅด๊ฒŒ ๋ณด์—ฌ๋“œ๋ฆฌ๊ธฐ ์œ„ํ•ด, ์ž„์˜๋กœ ํƒ€์ด๋จธ์˜ ์‹œ๊ฐ„์„ 25๋ถ„๊ณผ 5๋ถ„ โ†’ ๋ชจ๋‘ 2์ดˆ๋กœ ์กฐ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

## ๐Ÿ› ๏ธ About Code
### 1. ManagedSetting ๊ถŒํ•œ ์š”์ฒญ

```swift
import SwiftUI
import FamilyControls

@main
struct ScreenTimeApp: App {
let center = AuthorizationCenter.shared

var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
Task {
do {
try await center.requestAuthorization(for: .individual)
} catch {
print("Fail: \(error)")
}
}
}
}
}
}
```

์—”ํŠธ๋ฆฌ ํฌ์ธํŠธ์ธ App ๊ตฌ์กฐ์ฒด์— FamilyControls์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ์š”์ฒญ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

iOS15์—์„œ๋Š” ๊ฐ€์กฑ ๊ตฌ์„ฑ์› ์ค‘ ๋ถ€๋ชจ ๊ถŒํ•œ์˜ ์Šน์ธ์ด ์žˆ์–ด์•ผ ์‹คํ–‰๊ฐ€๋Šฅ ํ–ˆ์ง€๋งŒ, iOS16 ๋ฒ„์ „๋ถ€ํ„ฐ ๊ฐ€์กฑ ๊ตฌ์„ฑ์›์˜ ์Šน์ธ ์—†์ด ์ž์‹ ์˜ iPhone์— ์Šค์Šค๋กœ ๊ถŒํ•œ์„ ์š”์ฒญํ™œ ์ˆ˜ ์žˆ๋„๋ก ์—…๋ฐ์ดํŠธ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค (.individual ์„ ํƒ)

### 2. ManagedSetting์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ์ฒด

```swift
import Foundation
import FamilyControls
import ManagedSettings

class ManagedSettingModel: ObservableObject {
static var shared = ManagedSettingModel()

let store = ManagedSettingsStore()

@Published var selectionToDiscourage: FamilyActivitySelection

init() {
selectionToDiscourage = FamilyActivitySelection()
}

...
}
```

๊ถŒํ•œ์„ ์ œ์–ดํ•˜๋Š” ๊ฐ์ฒด๋ฅผ ๋‹จ ํ•˜๋‚˜๋งŒ ๋‘๊ธฐ ์œ„ํ•ด SingleTonํŒจํ„ด์œผ๋กœ ManagedSetting๋ชจ๋ธ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์€ ํ•˜๋‚˜์˜ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค(๊ฐ์ฒด)๋งŒ ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์— ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด ์˜์—ญ์—์„œ ๋™์ผํ•œ ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์  ๋•๋ถ„์— ์œ ์ € ์ •๋ณด, ์•ฑ ์„ค์ •๊ณผ ๊ฐ™์ด, ์ธ์Šคํ„ด์Šค๋“ค์ด ๊ผฌ์ด๋Š” ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ๋ฉ”๋ชจ๋ฆฌ์˜ ๋‚ญ๋น„๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ƒํ™ฉ์—์„œ๋Š” ํŠนํžˆ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ œํ•œ๊ณผ ํ•ด์ œ๋ฅผ ์ œ์–ดํ•˜๋Š” ๊ฐ์ฒด๋Š” ๋‹จ ํ•˜๋‚˜๋งŒ ์กด์žฌํ•ด์•ผ ๊ผฌ์ด์ง€ ์•Š๊ณ , ์˜๋„์น˜ ์•Š์€ ์–ดํ”Œ ์ œํ•œ/ํ•ด์ œ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์„œ SingleTonํŒจํ„ด์„ ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ๋Š”

`ManagedSettingModel.shared.[ ๋ฉ”์„œ๋“œ ํ˜น์€ ํ”„๋กœํผํ‹ฐ ]` ์ฝ”๋“œ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

### 3. ์•ฑ ์ œํ•œ ๋ฐ ํ•ด์ œ ๊ธฐ๋Šฅ

```swift
import Foundation
import FamilyControls
import ManagedSettings

class ManagedSettingModel: ObservableObject {
...

func setShieldRestrictions() {
let applications = ManagedSettingModel.shared.selectionToDiscourage

storeshield.applications = applications.applicationTokens.isEmpty ? nil : applications.applicationTokens
store.shield.applicationCategories = applications.categoryTokens.isEmpty ? nil : ShieldSettings.ActivityCategoryPolicy.specific(applications.categoryTokens)
}

func freeShieldRestrictions() {
store.shield.applications = nil
store.shield.applicationCategories = nil
}

func isSelectionEmpty() -> Bool {
selectionToDiscourage.applicationTokens.isEmpty &&
selectionToDiscourage.categoryTokens.isEmpty &&
selectionToDiscourage.webDomainTokens.isEmpty
}

}
```

์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ œํ•œ, ํ•ด์ œํ•˜๊ธฐ ์œ„ํ•ด์„œ `ManagedSettingsStore()` ์˜ `sheild` ์˜ `applications` ์— ์„ ํƒํ•œ ์–ดํ”Œ์˜ ํ† ํฐ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๋น„์›Œ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์œ„ ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ

`setShieldRestrictions()` ๋ฉ”์„œ๋“œ ์„ ์–ธํ•˜๊ณ  ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ† ํฐ์„ ๋‹ด์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€๊ณ 

`freeShieldRestrictions()` ๋ฉ”์„œ๋“œ๋ฅผ ์„ ์–ธํ•˜๊ณ  ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ† ํฐ์„ ๋น„์šธ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ด์ œ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋“ค์„ ํ•„์š”ํ•œ ์‹œ์ ์— ์‹คํ–‰ ์‹œ์ผœ์ฃผ๋ฉด ์•ฑ์„ ์ œํ•œํ•˜๊ฑฐ๋‚˜ ํ•ด์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

### 4. ์ œํ•œ ํ•  ์•ฑ์„ ์„ ํƒํ•˜๋Š” ๋ฐฉ๋ฒ•

```swift
import SwiftUI
import FamilyControls

struct RoutinePrepareView: View {
@EnvironmentObject var managedSettingModel: ManagedSettingModel
@State var isPresented = false

var body: some View {
VStack { ... }
}
.familyActivityPicker(isPresented: $isPresented,
selection: $managedSettingModel.selectionToDiscourage)

}
}
```

View๋ฅผ ๊ทธ๋ฆด ๋•Œ, ์ตœ์ƒ์œ„ ๋ทฐ์— ์•„๋ž˜ ์ˆ˜์ •์ž๋ฅผ ์ž‘์„ฑํ•˜๊ณ 

์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” true๋กœ ๋ฐ”๋€” ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ๊ฐ’

๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” ์„ ํƒํ•œ ์•ฑ์˜ ํ† ํฐ์„ ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ๊ฐ’

์„ ๋Œ€์ž…ํ•˜๋ฉด ๋ฏธ๋ฆฌ ๊ตฌํ˜„๋œ, ์ œํ•œ ํ•  ์•ฑ์„ ์„ ํƒํ•˜๋Š” Modal๋ทฐ๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

```swift
.familyActivityPicker(isPresented: Binding,
selection: Binding)
```

๊ทธ๋ฆฌ๊ณ  ๊ทธ Modal ๋ทฐ์—์„œ ์•ฑ์„ ์„ ํƒํ•˜๋ฉด ์•ฑ์˜ ํ† ํฐ๋“ค์ด ๋‹ด๊ธฐ๊ฒŒ ๋˜๊ณ 

ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ๋˜๋Š” ๋ฒ„ํŠผ์—์„œ`setShieldRestrictions()` ์‹คํ–‰ํ•˜๋ฉด, ์„ ํƒํ•œ ์–ดํ”Œ๋“ค์€ ์‚ฌ์šฉ์ด ์ œํ•œ ๋ฉ๋‹ˆ๋‹ค.