๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ’ป Programming ๊ฐœ๋ฐœ/๐ŸŽ iOS ๊ฐœ๋ฐœ, Swift

[SwiftUI] Focus ์— ๊ด€ํ•˜์—ฌ 1 - WWDC 2023 ์˜์ƒ ์ •๋ฆฌ

by kimdee 2023. 9. 9.
๋ฐ˜์‘ํ˜•

 

์‹œ์ž‘ํ•˜๋ฉฐ

SwiftUI ๋ฅผ ์กธ์—…ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด๊ณ  ๋‚˜์„œ, ์ข€ ๋” ๊นŠ์ด์žˆ๊ฒŒ, ๊ธฐ๋ณธ๊ธฐ๋ฅผ ํƒ„ํƒ„ํžˆ ๊ณต๋ถ€ํ•ด์•ผ๊ฒ ๋‹ค๋Š” ๊ฒฐ์‹ฌ๋งŒ ํ•œ ์ง€ ์–ด์–ธ 5๊ฐœ์›”์ด ์ง€๋‚ฌ๊ณ , ๊ฐœ์ธ์‚ฌ์™€ ํšŒ์‚ฌ์ผ์— ํœฉ์“ธ๋ฆฌ๋ฉฐ ์‚ด๋‹ค๊ฐ€ ์ด์ œ์•ผ ์ •์‹ ์ฐจ๋ฆฌ๊ณ  SwiftUI๋ฅผ ๋‹ค์‹œ ๊ณต๋ถ€ํ•˜๊ณ ์ž WWDC 2023 ์˜์ƒ ์ค‘์— SwiftUI ์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.

 

์ด๋ฒˆ์— SwiftUIํŒ€์—์„œ ์•ฑ์˜ Focus(์ดํ•˜ ํฌ์ปค์Šค, ์ดˆ์ ์ด๋ผ๋Š” ์ข‹์€ ํ•œ๊ตญ์–ด๊ฐ€ ์žˆ์ง€๋งŒ API ์ด๋ฆ„ ์ž์ฒด๊ฐ€ ํฌ์ปค์Šค์ด๊ธฐ ๋•Œ๋ฌธ์—) ๊ฒฝํ—˜์„ ๋งŒ๋“ค์–ด์ค„ ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ํˆด์„ ๊ฐ€์ง€๊ณ  ๋Œ์•„์™”๋‹ค. ํฌ์ปค์Šค ๋“œ๋ฆฌ๋ธ ๊ฒฝํ—˜์ด๋ผ๋Š”๊ฒŒ ์–ด๋–ค๊ฑด์ง€, ์ปค์Šคํ…€ ๋ทฐ์˜ ํฌ์ปค์Šค ์ธํ„ฐ๋ž™์…˜๊ณผ ํ‚ค๋ณด๋“œ ์ธํ’‹์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ณ ์ž ํ•œ๋‹ค.

 

์ด ๊ธ€์€ ์•„๋ž˜ WWDC2023 ์˜์ƒ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ •๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

https://developer.apple.com/videos/play/wwdc2023/10162/

 

The SwiftUI cookbook for focus - WWDC23 - Videos - Apple Developer

The SwiftUI team is back in the coding

developer.apple.com

 

Focus API๊ฐ€ ๋ฌด์—‡์ผ๊นŒ?

ํฌ์ปค์Šค๋Š” ํ‚ค๋ณด๋“œ์˜ ํ‚ค๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ, ์• ํ”Œ TV ๋ฆฌ๋ชจ์ฝ˜์„ ์Šค์™€์ดํ”„ ํ–ˆ์„ ๋•Œ, ์™€์น˜์—์„œ ๋””์ง€ํ„ธ ํฌ๋ผ์šด์„ ์ผฐ์„ ๋•Œ ๋“ฑ์— ์–ด๋–ป๊ฒŒ ๋ฐ˜์‘ํ• ์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ํˆด์ด๋‹ค. ์ด๋Ÿฌํ•œ ์ธํ’‹ ๋ฐฉ๋ฒ•์€ ๊ณตํ†ต์ ์œผ๋กœ ์ค‘์š”ํ•œ ๋””ํ…Œ์ผ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋ฐ, ์ธํ’‹ ์žฅ์น˜์—์„œ๋Š” ์˜จ์Šคํฌ๋ฆฐ์—์„œ ์–ด๋–ค ์ปจํŠธ๋กค์ด ๋˜๋Š”์ง€์— ๋Œ€ํ•ด ์ถฉ๋ถ„ํ•œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜์ง€๋Š” ์•Š๋Š”๋‹ค๋Š” ์ ์ด๋‹ค.

 

๋งˆ์šฐ์Šค๋‚˜ ํŠธ๋ž™ํŒจ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์˜จ ์Šคํฌ๋ฆฐ ์ปค์„œ๋Š” ์ธํ„ฐ๋ž™์…˜ํ•  ํƒ€๊ฒŸ์„ ์ฐพ๊ธฐ ์œ„ํ•ด ํ™”๋ฉด ์ขŒํ‘œ์™€ ์—ฐ๊ฒฐ๋œ๋‹ค. ํฌ์ปค์Šค๋Š” ์ปค์„œ ์—†์ด๋„ ์ง์ ‘ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ๋ทฐ์— ํฌ์ปค์Šค๊ฐ€ ์žˆ์œผ๋ฉด ์‹œ์Šคํ…œ์ด ์ž…๋ ฅ์— ์‘๋‹ตํ•˜๊ธฐ ์œ„ํ•œ ์‹œ์ž‘์ง€์ ์œผ๋กœ ์žก๊ฒŒ ๋œ๋‹ค.

  

ํฌ์ปค์Šค๊ฐ€ ๋ณด์—ฌ์ง€๋Š” ํ˜•ํƒœ

ํฌ์ปค์Šค๋Š” ๋‹จ์ˆœํžˆ ๋””ํ…Œ์ผ์˜ ๊ตฌํ˜„ ๋ฟ ์•„๋‹ˆ๋ผ ์•ฑ์„ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์—๊ฒŒ๋„ ์ค‘์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํฌ์ปค์Šค๋œ ๋ทฐ๋Š” ํŠน๋ณ„ํ•˜๊ฒŒ ๊ฐ•์กฐ๋˜์„œ ํ‘œ์‹œ๊ฐ€ ๋œ๋‹ค.

macOS ์—์„œ๋Š” ํฌ์ปค์Šค๋œ ๋ทฐ์— ๋ณด๋”๊ฐ€ ์ƒ๊ธฐ๊ณ  ํ‚ค๋ณด๋“œ ์ธํ’‹์„ ๋ฐ›์„๊ฑฐ๋ผ๋Š” ๊ฑธ ๋ณด์—ฌ๊ณ , watchOS๋Š” ์ดˆ๋ก์ƒ‰ ๋ณด๋”๊ฐ€ ๊ทธ๋ ค์ง€๊ณ  tvOS๋Š” ๋‹ค๋ฅธ ์ปจํŠธ๋กค๋ณด๋‹ค ๋„์›Œ์ ธ ์žˆ๋Š”(=ํ›„๋ฒ„) ๋ชจ์–‘์ƒˆ๋กœ ํ‘œ์‹œ๋œ๋‹ค.

 

ํฌ์ปค์Šค์˜ ์—ญํ•  

  • ์‚ฌ์šฉ์ž ์ž…๋ ฅ(ํ‚ค๋ณด๋“œ, ๋ฆฌ๋ชจ์ฝ˜ ๋“ฑ) ์ด ์–ด๋””๋กœ ๊ฐˆ์ง€ ์•Œ๋ ค์คŒ
  • ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์˜ ์–ด๋–ค ๋ถ€๋ถ„๊ณผ ์ธํ„ฐ๋ž™ํŒ… ํ•˜๊ณ  ์žˆ๋Š”์ง€ ๋น„์ฃผ์–ผ ๋ ˆํผ๋Ÿฐ์Šค ํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•จ

 

ํฌ์ปค์Šค์˜ ๋™์ž‘

ํฌ์ปค์Šค๋Š” ์ปค์„œ์™€ ๋งŽ์ด ์œ ์‚ฌํ•˜๊ฒŒ ๋™์ž‘ํ•˜๊ฒŒ ๋จ. ๋งˆ์šฐ์Šค ์ปค์„œ๋กœ ํ™”๋ฉด์˜ ์ง€์ ์„ ๊ณ„์† ์ถ”์ ํ•˜๋Š” ๊ฒƒ ๋Œ€์‹ ์—, ํฌ์ปค์Šค๋Š” UI์˜ ์–ด๋–ค ํŒŒํŠธ๊ฐ€ ํฌ์ปค์Šค ์ธํ’‹์˜ ํƒ€๊ฒŸ์ด ๋˜๋Š”์ง€๋ฅผ ์ถ”์ ํ•จ.

 

ํฌ์ปค์Šค์™€ ๊ด€๋ จ๋œ ์š”์†Œ๋“ค 

 

Focusable Views

ํฌ์ปค์Šค ์ธํ’‹์— ์‘๋‹ตํ•  ๋•Œ ์‹œ์ž‘์ง€์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ทฐ. ๋‹ค๋ฅธ ์ƒํ™ฉ๊ณผ ๋‹ค๋ฅธ ์ด์œ ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ปจํŠธ๋กค์ด ํฌ์ปค์Šค๋  ์ˆ˜ ์žˆ๋‹ค.

macOS์™€ ipadOS์˜ ํ…์ŠคํŠธํ•„๋“œ์™€ ๋ฒ„ํŠผ์„ ๋น„๊ตํ•ด๋ณผ ๋•Œ, ํ…์ŠคํŠธํ•„๋“œ๋Š” ํ•ญ์ƒ ํฌ์ปค์Šค๊ฐ€ ์žˆ์Œ. ํƒญ ํ•˜๊ฑฐ๋‚˜ ํ˜น์€ Tab ํ‚ค๋ฅผ ์ด์šฉํ•ด ์ด์ „ ์ปจํŠธ๋กค์—์„œ ์˜ฎ๊ฒผ๋“ , ์ด๋Ÿฐ ์ข…๋ฅ˜์˜ ์ปจํŠธ๋กค์€ ํŽธ์ง‘ํ•˜๊ธฐ ์œ„ํ•ด ํฌ์ปค์Šค๊ฐ€ ์ œ๊ณต๋จ. ์™œ๋ƒํ•˜๋ฉด ๊ณ„์†ํ•ด์„œ ํฌ์ปค์Šค ์ธํ’‹์„ ์บก์ณํ•ด์•ผ ๋˜๊ธฐ ๋•Œ๋ฌธ์—.

๋ฒ„ํŠผ์˜ ๊ฒฝ์šฐ๋Š” ์ข€ ๋‹ค๋ฅธ๋ฐ, ๋ฒ„ํŠผ์€ ํด๋ฆญ๊ณผ ํƒญ์„ ๋‹ค๋ฃจ๊ธฐ ๋•Œ๋ฌธ์— macOS์™€ iPad์—์„œ๋Š” ๋ฒ„ํŠผ์„ ํƒญํ–ˆ๋‹ค๊ณ  ํฌ์ปค์Šค๊ฐ€ ๊ฐ€์ง€ ์•Š๋Š”๋‹ค. ํฌ์ปค์Šค๋ฅผ ์ฃผ๊ธฐ ์œ„ํ•ด์„œ๋Š” ์‹œ์Šคํ…œ ์„ค์ •์—์„œ ํ‚ค๋ณด๋“œ ์„ธํŒ…์—์„œ ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ์ผ  ํ›„ Tabํ‚ค๋ฅผ ํ†ตํ•ด ๋ฒ„ํŠผ์— ํฌ์ปค์Šค๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ ๋‹ค์Œ ์ŠคํŽ˜์ด์Šค ๋ฐ”๋ฅผ ๋ˆŒ๋Ÿฌ์„œ ๋ฒ„ํŠผ์„ ํ™œ์„ฑํ™” ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ™œ์„ฑํ™”ํ•  ๋•Œ ๋ฒ„ํŠผ์— ์ดˆ์ ์ด ์ƒ๊ธฐ๋Š”๋ฐ ์‚ฌ์‹ค ์ด๋Ÿฐ ์ข…๋ฅ˜์˜ ์ปจํŠธ๋กค์€ ๋ณธ์ธ๋“ค ์ž‘์—…์„ ํ•˜๊ธฐ ์œ„ํ•ด ํฌ์ปค์Šค๊ฐ€ ํ•„์š”ํ•˜์ง„ ์•Š๋‹ค. ๊ทธ๋ ‡์ง€๋งŒ ์‹œ์Šคํ…œ์ด ํ—ˆ์šฉํ•  ๊ฒฝ์šฐ, ํด๋ฆญ๊ณผ ํƒญ์— ํฌ์ปค์Šค ๋“œ๋ฆฌ๋ธ์œผ๋กœ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํฌ์ปค์Šค๋ฅผ ์ทจํ•  ์ˆœ ์žˆ์Œ.

 

iOS 17, macOS Sonoma์—์„œ, ํฌ์ปค์Šค ์‹œ์Šคํ…œ์— ์ปค์Šคํ…€ ์ปจํŠธ๋กค์ด ๋™์ฐธํ•  ์ˆ˜ ์žˆ๋Š” ์ƒˆ API๋ฅผ ์ œ๊ณตํ•จ. focusable ๋ทฐ ๋ชจ๋””ํŒŒ์ด์–ด๋ฅผ ์ ์šฉํ•˜๋ฉด ์ปจํŠธ๋กค์—์„œ ์ œ๊ณตํ•˜๋Š” ํฌ์ปค์Šค ์ธํ„ฐ๋ž™์…˜์˜ ์ข…๋ฅ˜๋ฅผ ์ง€์ •ํ•จ์œผ๋กœ์จ ๊ฒฐ๊ณผ๋กœ ๋‚˜์˜ค๋Š” ๋™์ž‘์„ ๋ฏธ์„ธ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Œ.

 

๊ณ„์†ํ•ด์„œ ํฌ์ปค์Šค๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ปจํŠธ๋กค์˜ ๊ฒฝ์šฐ .focusable(interactions: .edit) ์œผ๋กœ ์„ค์ •ํ•˜๊ณ , ์ง์ ‘์ ์ธ ํฌ์ธํ„ฐ ํ™œ์„ฑํ™”๋ฅผ ๋Œ€์‹ ํ•˜๊ธฐ ์œ„ํ•ด์„œ ํฌ์ปค์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปจํŠธ๋กค์€ .focusable(interactions.activate)์œผ๋กœ ์„ค์ •ํ•จ. 

 

// Focusable views

struct RecipeGrid: View {
    var body: some View {
        LazyVGrid(columns: [GridItem(), GridItem()]) {
            ForEach(0..<4) { _ in Capsule() }
        }
        .focusable(interactions: .edit) // ๐Ÿ“Œ
    }
}

struct RatingPicker: View {
    var body: some View {
        HStack { Capsule() ; Capsule() }
            .focusable(interactions: .activate) // ๐Ÿ“Œ
    }
}

์ธ์ž๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฉด, ์‹œ์Šคํ…œ์€ ๋ชจ๋“  ์ธํ„ฐ๋ž™์…˜์— ํฌ์ปค์Šค๋ฅผ ์ปจํŠธ๋กคํ•˜๋„๋ก ํ•จ.

 

macOS Sonoma ์ด์ „์—, focusable ๋ชจ๋””ํŒŒ์ด์–ด๋Š” ํ™œ์„ฑํ™”๋œ ๋งฅ๋ฝ์—์„œ๋งŒ ์ œ๊ณตํ–ˆ๋‹ค. ์ด๋ฏธ macOS ์ฝ”๋“œ์—์„œ focuasable ๋ชจ๋””ํŒŒ์ด์–ด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฉด ์ƒˆ ๋™์ž‘์ด ์œ ์ฆˆ์ผ€์ด์Šค์— ๋งž๋Š”์ง€ ๊ฒ€์ฆํ•ด์•ผ๋  ๊ฒƒ.

 

interactions ์ธ์ž๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์—…๋ฐ์ดํŠธ ํ•ด์•ผ๋  ์ˆ˜ ์žˆ์Œ.

 

ํฌ์ปค์Šค์˜ ์ƒํƒœ 

์ˆœ๊ฐ„๊ณผ ์ˆœ๊ฐ„์˜ ํฌ์ปค์Šค ์‹œ์Šคํ…œ ์ƒํƒœ. FocusState ์‹œ์Šคํ…œ์€ ์–ด๋–ค ๋ทฐ๊ฐ€ ํฌ์ปค์Šค๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ๊ณ„์† ์ถ”์ ํ•˜๊ณ , ์•ฑ์€ ์ด ์ •๋ณด๋ฅผ ๊ณ„์† ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉด์„œ ์ธํ’‹์„ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ๋ทฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์Šคํƒ€์ผํ• ์ง€ ๊ฒฐ์ •ํ•จ. ์‹œ์Šคํ…œ ์ƒํƒœ๋ฅผ ๊ด€์ธกํ•˜๊ธฐ ์œ„ํ•ด์„œ ํŠน์ • ๋ทฐ์—์„œ์˜ ํฌ์ปค์Šค์™€ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐ”์ธ๋”ฉ์„ ๋งŒ๋“ค์–ด์ฃผ์–ด์•ผ ํ•œ๋‹ค. ๋ทฐ์—์„œ ์ด ๋ฐ”์ธ๋”ฉ์„ ์ฝ๊ณ  ํฌ์ปค์Šค๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์•Œ๋ฆผ์„ ๋ฐ›๊ฒŒ ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋ทฐ๊ฐ€ ํฌ์ปค์Šค๋˜๋ฉด, ๋˜๋Š” ํฌ์ปค์Šค๊ฐ€ ์‚ฌ๋ผ์ง€๋Š” ์•Œ๋ฆผ.

// Focus state

struct GroceryListView: View {
    @FocusState private var isItemFocused
    @State private var itemName = ""

    var body: some View {
        TextField("Item Name", text: $itemName)
            .focused($isItemFocused)

        Button("Done") { isItemFocused = false }
            .disabled(!isItemFocused)
    }
}

ํฌ์ปค์Šค ์ƒํƒœ ํ”„๋กœํผํ‹ฐ์ธ Boolean ๊ฐ’์€ ํ•˜๋‚˜์˜ ๋ทฐ๊ฐ€ ํฌ์ปค์Šค ๋˜์–ด ์žˆ๋Š”์ง€๋งŒ์„ ์•Œ๋ ค์ค€๋‹ค.

 

๋งŒ์•ฝ ๋” ๋ณต์žกํ•œ ์ผ€์ด์Šค์˜ ๊ฒฝ์šฐ, ์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•จ.

 

 

 

Focused Values

Focused Value API๋Š” ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ฆฌ๋ชจํŠธ ๋ถ€๋ถ„์˜ ์—ฐ๊ฒฐ์— ๋ฐ์ดํ„ฐ ์˜์กด์„ฑ์„ ์–ด๋–ป๊ฒŒ ๊ตฌ์ถ•ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด์ค€๋‹ค.

์ด API๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์•กํ‹ฐ๋ธŒ ์”ฌ์—์„œ ์ผ์–ด๋‚˜๋Š” ์ผ๋“ค์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์•ฑ์˜ ์ปค๋งจ๋“œ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•  ์ˆ˜ ์žˆ๋‹ค. Focused Value๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์š”์†Œ๋“ค์˜ ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

 

์ปค์Šคํ…€์„ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์„œ ๋ฉ”์ธ ๋ฉ”๋‰ด ์ฝ˜ํ…ํŠธ์— ์ ์šฉํ•˜๋„๋ก ํ•ด๋ณผ ๊ฒƒ.

 

Focused Values๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฑด ์ปค์Šคํ…€ Environment ํ‚ค์™€ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋น„์Šทํ•จ.

 

FocusedValueKey ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•ด์„œ ์ƒˆ ํ‚ค๋ฅผ ๋งŒ๋“ค๊ณ  ํ™•์žฅํ•ด์„œ ์ƒˆ๋กœ์šด ํ‚ค๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ๋Š” ์—ฐ์‚ฐ ํ”„๋กœํผํ‹ฐ๋ฅผ ์„ ์–ธํ•œ๋‹ค.

 

// Focused values

struct SelectedRecipeKey: FocusedValueKey {
    typealias Value = Binding<Recipe>
}

extension FocusedValues {
    var selectedRecipe: Binding<Recipe>? {
        get { self[SelectedRecipeKey.self] }
        set { self[SelectedRecipeKey.self] = newValue }
    }
}

struct RecipeView: View {
    @Binding var recipe: Recipe

    var body: some View {
        VStack {
            Text(recipe.title)
        }
        .focusedSceneValue(\.selectedRecipe, $recipe)
// ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š”, ํ•ด๋‹น ์”ฌ์˜ ๋ทฐ์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๊ฐ’์€ ๊ฐ’์ด๋‚˜ ๋ฐ”์ธ๋”ฉ, ObservableObject๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค. 
    }
}

struct RecipeCommands: Commands {
    @FocusedBinding(\.selectedRecipe) private var selectedRecipe: Recipe?

    var body: some Commands {
        CommandMenu("Recipe") {
            Button("Add to Grocery List") {
                if let selectedRecipe {
                    addRecipe(selectedRecipe)
                }
            }
            .disabled(selectedRecipe == nil)
        }
    }

    private func addRecipe(_ recipe: Recipe) { /* ... */ }
}

struct Recipe: Hashable, Identifiable {
    let id = UUID()
    var title = ""
    var isFavorite = false
}

๋ทฐ ๋ชจ๋””ํŒŒ์ด์–ด๋ฅผ ํ•ด๋‹น ๋ฐ์ดํ„ฐ์™€ ์—ฐ๊ฒฐํ•ด์„œ, ํฌ์ปค์Šค๊ฐ€ ๋ทฐ ๊ณ„์ธต์— ์†ํ•˜๊ฒŒ ํ•œ๋‹ค.

Environment ๊ฐ’๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ๋‹ค์ด๋‚˜๋ฏน ํ”„๋กœํผํ‹ฐ๋ฅผ ์„ ์–ธํ•จ์œผ๋กœ์จ Focused Value์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” Focused Value๊ฐ€ ๋ฐ”์ธ๋”ฉ์ด๋ฏ€๋กœ @FocusedBinding ํ”„๋กœํผํ‹ฐ ๋ž˜ํผ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์—ฌ๊ธฐ์— ์ปค์Šคํ…€ ํ‚ค ํŒจ์Šค๋ฅผ ์ œ๊ณตํ•  ๊ฒƒ.

 

@FocusedBinding ์€ ํฌ์ปค์Šค๋œ ๋ทฐ์™€ ๋ทฐ์˜ ์กฐ์ƒ์„ ์‚ดํ”ผ๊ณ  ํ‚ค์™€ ์—ฐ๊ด€๋œ ๋ฐ”์ธ๋”ฉ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. ํ”„๋กœํผํ‹ฐ ๋ž˜ํผ๋Š” ์ž๋™์ ์œผ๋กœ ๋ฐ”์ธ๋”ฉ์„ ํ’€์–ด์„œ ๋ฐ”์šฐ๋”ฉ ๊ฐ’์„ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

 

์ด์ œ ์ด ์ƒˆ ํ”„๋กœํผํ‹ฐ๋ฅผ view์˜ ๋ฐ”๋””์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ. ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด์„œ ํฌ์ปค์Šค๊ฐ€ ๋‹ค๋ฅธ ์ปจํŠธ๋กค๊ณผ ํ˜น์€ ํ™œ์„ฑํ™”๋œ ๋‹ค๋ฅธ ์œˆ๋„์šฐ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ์‹œ์Šคํ…œ์€ ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ์—์„œ ์ฐพ์€ ๊ฐ’๋“ค์„ ๋ฐ˜์˜ํ•˜์—ฌ ๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ฒŒ ๋  ๊ฒƒ.

 

Focused Sections 

ํฌ์ปค์Šค ์„น์…˜์„ ์ด์šฉํ•˜๋ฉด, ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜(์• ํ”Œ TV ๋ฆฌ๋ชจ์ฝ˜์„ ์Šค์™€์ดํ”„ํ•˜๊ฑฐ๋‚˜, ํ‚ค๋ณด๋“œ์—์„œ Tabํ‚ค๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ ๋“ฑ) ํฌ์ปค์Šค ์ด๋™๋ฐฉ์‹์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Œ.

 

๊ธฐ๋ณธ์ ์œผ๋กœ ํฌ์ปค์Šค๋Š” ํ™”๋ฉด์˜ leading ๊ฐ€์žฅ์ž๋ฆฌ์—์„œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ตœ์ƒ๋‹จ์˜ ์ปจํŠธ๋กค๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค.

 

์—ฌ๊ธฐ์„œ Tab์„ ๋ˆ„๋ฅด๋ฉด ํฌ์ปค์Šค๊ฐ€ ํ˜„์žฌ ๋กœ์ผ€์ผ์˜ ๋ ˆ์ด์•„์›ƒ ์ˆœ์„œ์— ๋”ฐ๋ผ์„œ ๋‹ค์Œ ์ปจํŠธ๋กค๋กœ ์ด๋™ํ•œ๋‹ค.

 

Leading
: ํ•œ๊ตญ์–ด, ์˜์–ด์ฒ˜๋Ÿผ Left-to-right๋กœ ์ฝ๋Š” ๊ณณ์—์„œ๋Š” ์™ผ์ชฝ์ด์ง€๋งŒ ์•„๋ž์–ด๊ฐ™์ด Right-to-left ์ธ ๋‚˜๋ผ์˜ ๊ฒฝ์šฐ ์˜ค๋ฅธ์ชฝ
Locale
: ์žฅ์†Œ. ์ข€ ๋”๋Š” ํ•ด๋‹น ์ง€์—ญ๊ณผ ๋ฌธํ™”๊ถŒ์„ ์˜๋ฏธํ•จ. ์œ„์ฒ˜๋Ÿผ RTL์ธ ์•„๋ž์–ด๊ถŒ ๊ตญ๊ฐ€์˜ ๊ฒฝ์šฐ, ๋‹ค์Œ์œผ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋˜๋ฉด ์˜ค๋ฅธ์ชฝ์—์„œ → ์™ผ์ชฝ์œผ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋  ๊ฒƒ.

  

์Šคํฌ๋ฆฐ์˜ ๋งˆ์ง€๋ง‰ ์ปจํŠธ๋กค์— ๋„๋‹ฌํ•˜๋ฉด, Tab์„ ๋ˆ„๋ฅผ๋•Œ ์‹œํ€€์Šค์˜ ๋งจ ์ฒ˜์Œ์œผ๋กœ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ฒŒ ๋œ๋‹ค.

์• ํ”Œ TV์—์„œ ํฌ์ปค์Šค ์ด๋™์€ ๋ฐฉํ–ฅ์„ฑ์ด ์žˆ๋‹ค. ์œ„, ์•„๋ž˜, ์ขŒ, ์šฐ๋กœ ์Šค์™€์ดํ”„ํ•ด์„œ ์ปจํŠธ๋กค์„ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ขŒ์šฐ๋กœ ๋””์ €ํŠธ๊ฐ„์„ ์ด๋™ํ•  ์ˆœ ์žˆ์ง€๋งŒ, ์žฅ๋ณด๊ธฐ ๋ฆฌ์ŠคํŠธ์— ํฌ๋ ˜๋ธŒ๋ฅ„๋ ˆ๋ฅผ ๋„ฃ๊ณ  ์‹ถ์€๋ฐ ์Šค์™€์ดํ”„ ๋‹ค์šดํ•  ์ˆœ ์—†์Œ. ์™œ๋ƒํ•˜๋ฉด ์ด ๋ฒ„ํŠผ์ด ๋ฐ”๋กœ ํฌ๋ ˜๋ธŒ๋ฅ„๋ ˆ ์•„๋ž˜์— ์œ„์น˜ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—.

 

ํฌ์ปค์Šค ํƒ€๊ฒŸ์„ ์ •๋ ฌํ•˜๊ธฐ ์œ„ํ•ด์„œ, ์•„๋ž˜ ๋ฒ„ํŠผ์˜ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํฌ์ปค์Šค ์„น์…˜์œผ๋กœ ๋งˆํฌํ•  ๊ฒƒ. ํฌ์ปค์Šค ์„น์…˜์ด ์ด๋™ ์ œ์Šค์ณ์˜ ํƒ€๊ฒŸ์ด ๋˜์ง€๋งŒ ํฌ์ปค์Šค๊ฐ€ ๋˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ํฌ์ปค์Šค๋  ์ˆ˜ ์žˆ๋Š” ์ฝ˜ํ…ํŠธ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฐ€์ด๋“œ๊ฐ€ ๋œ๋‹ค.

 

ํฌ์ปค์Šค ์„น์…˜์„ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํฌ์ปค์Šค ์„น์…˜์ด ์ฝ˜ํ…ํŠธ๋“ค๋ณด๋‹ค ๋” ๋งŽ์€ ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜๊ณ  ์žˆ์–ด์•ผ ํ•œ๋‹ค.

์ง€๊ธˆ ๊ฐ™์€ ๊ฒฝ์šฐ ๋ฒ„ํŠผ ์ „ํ›„๋กœ Spacer()๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์Šคํƒ์ด ์Šคํฌ๋ฆฐ ๋„ˆ๋น„์— ๋งž๊ฒŒ ์ปค์ง€๋„๋ก ํ•  ๊ฒƒ.

๋” ํฐ ํฌ์ปค์Šค ํƒ€๊ฒŸ์ด ๋˜์—ˆ์œผ๋ฏ€๋กœ, ์–ด๋Š๋•Œ๊ณ  ์Šค์™€์ดํ”„ ๋‹ค์šดํ•ด์„œ ์•„๋ž˜ ๋ฒ„ํŠผ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

 

// Focus sections

struct ContentView: View {
    @State private var favorites = Recipe.examples
    @State private var selection = Recipe.examples.first!

    var body: some View {
        VStack {
            HStack {
                ForEach(favorites) { recipe in
                    Button(recipe.name) { selection = recipe }
                }
            }

            Image(selection.imageName)

            HStack {
                Spacer()
                Button("Add to Grocery List") { addIngredients(selection) }
                Spacer()
            }
            .focusSection()
        }
    }

    private func addIngredients(_ recipe: Recipe) { /* ... */ }
}

struct Recipe: Hashable, Identifiable {
    static let examples: [Recipe] = [
        Recipe(name: "Apple Pie"),
        Recipe(name: "Baklava"),
        Recipe(name: "Crème Brûlée")
    ]

    let id = UUID()
    var name = ""
    var imageName = ""
}

 

Focus๋ฅผ ์ปจํŠธ๋กคํ•˜๊ธฐ

์ด ๋ถ€๋ถ„์€ ์•„๋ž˜ 2ํŽธ์—์„œ ๋งˆ์ € ๋‹ค๋ฃจ์—ˆ์Šต๋‹ˆ๋‹ค. 

 

 

https://kimdee.tistory.com/entry/SwiftUI-Focus-์—-๊ด€ํ•˜์—ฌ-2-WWDC-2023-์˜์ƒ-์ •๋ฆฌ

 

[SwiftUI] Focus ์— ๊ด€ํ•˜์—ฌ 2 - WWDC 2023 ์˜์ƒ ์ •๋ฆฌ

์ด ๊ธ€์€ ์•„๋ž˜ WWDC2023 ์˜์ƒ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ •๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. https://developer.apple.com/wwdc23/10162 The SwiftUI cookbook for focus - WWDC23 - Videos - Apple Developer The SwiftUI team is back in the coding developer.apple.com ํฌ์ปค์Šค API์™€

kimdee.tistory.com

 

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.


 

 

 

 

์˜ค๋Š˜๋„ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. 

 

๊ถ๊ธˆํ•˜๊ฑฐ๋‚˜ ๋‚˜๋ˆ„๊ณ  ์‹ถ์€ ์–˜๊ธฐ๊ฐ€ ์žˆ์œผ์‹œ๋ฉด ๋Œ“๊ธ€๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”!

์žฌ๋ฐŒ๊ฒŒ ์ฝ์œผ์…จ๋‹ค๋ฉด ๊ณต๊ฐ๊ณผ ๊ตฌ๋…์€ ํฐ ํž˜์ด ๋ฉ๋‹ˆ๋‹ค. 

 

ํ•ญ์ƒ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

 

 

์ด ๊ธ€์€ doy.oopy.io ์—๋„ ๋ฐœํ–‰๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. 

 

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€