๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/SwiftUI

[SwiftUI] ์ž์—ฐ์Šค๋Ÿฌ์šด ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌํ˜„ํ•˜๊ธฐ (feat. matchedGeometryEffect,Namespace)

by Fomagran ๐Ÿ’ป 2022. 3. 28.
728x90
๋ฐ˜์‘ํ˜•

์•ˆ๋…•ํ•˜์„ธ์š” Foma ๐Ÿ’ป ์ž…๋‹ˆ๋‹ค!

 

ํ‰์†Œ์— AppStore๋ฅผ ๋ณด๋ฉด ํ™”๋ฉด ๊ฐ„ ์ „ํ™˜ํ•  ๋•Œ ์ž์—ฐ์Šค๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋งŽ์ด ๋ณด์•˜๋Š”๋ฐ, '์ด๊ฑด ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ์„๊นŒ' ๋ผ๋Š” ์ƒ๊ฐ์ด ๋งŽ์ด ๋“ค์–ด ๊ธ€์„ ์ •๋ฆฌํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋ฐ”๋กœ ์‹œ์ž‘ํ• ๊ฒŒ์š”~


Preview

 


@Namespace

 

ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„  @Namespace ํ”„๋กœํผํ‹ฐ ๋ž˜ํผ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

 

@Namespace๋Š” ๊ณต์‹ ๋ฌธ์„œ์—” ์•„๋ž˜์™€ ๊ฐ™์ด ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

 

์†์„ฑ์„ ํฌํ•จํ•˜๋Š” ๊ฐœ์ฒด(์˜ˆ: view)์˜ ์˜๊ตฌ ID๋กœ ์ •์˜๋œ ๋„ค์ž„์ŠคํŽ˜์ด์Šค์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ํ—ˆ์šฉํ•˜๋Š” ๋™์  ์†์„ฑ ์œ ํ˜•์ž…๋‹ˆ๋‹ค.

 

 

๊ฐ„๋‹จํ•˜๊ฒŒ ๋งํ•˜๋ฉด ๊ฐ์ฒด์˜ ์ •๋ณด๋ฅผ ID์™€ ํ•จ๊ป˜ ๊ธฐ์–ตํ•˜๊ณ  ์žˆ๋Š” ๋ž˜ํผ์ž…๋‹ˆ๋‹ค.

 

์ด ID๋ฅผ ํ†ตํ•ด์„œ ์ •๋ณด๋ฅผ ๋‹ค๋ฅธ ๋ทฐ์™€ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๊ฒƒ์„ ํ†ตํ•ด ์ง€์˜ค๋ฉ”ํŠธ๋ฆฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๊ฐ€๋Šฅํ•ด ์ง‘๋‹ˆ๋‹ค.

 

์„ ์–ธํ•˜๋Š” ๋ฐฉ์‹์€ ์•„๋ž˜์™€ ๊ฐ™์ด @Namespace๋ฅผ ๋ถ™์—ฌ์ฃผ๊ณ  ๋ณ€์ˆ˜์˜ ์ด๋ฆ„์„ ์ •ํ•ด์ค๋‹ˆ๋‹ค.

 

@Namespace var animation(์›ํ•˜๋Š” ์ด๋ฆ„)

matchedGeometryEffect

 

matchedGeometryEffect๋Š” ์›ํ•˜๋Š” ID์™€ namespace๋ฅผ ์ด์šฉํ•˜์—ฌ ํŠน์ • ๋ทฐ์— ์ง€์˜ค๋ฉ”ํŠธ๋ฆฌ ํšจ๊ณผ๋ฅผ ๋ถ€์—ฌํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

 

 

์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋ž˜์™€ ๊ฐ™์ด ์›ํ•˜๋Š” ๋ทฐ(๊ฐ์ฒด)์— id์™€ namespace๋ฅผ ํ•จ๊ป˜ ์ ์šฉ์‹œ์ผœ ์ค๋‹ˆ๋‹ค.

 

์ฃผ์˜ํ•˜์‹ค ์ ์€ ๋ฐ”๋€Œ๋Š” ์–ด๋–ค ๊ฒƒ์˜ ์œ„์— ์ ์šฉ์‹œ์ผœ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์•„๋ž˜ ์ฝ”๋“œ์—์„  ํ™”๋ฉด์˜ ํ”„๋ ˆ์ž„์„ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด๋ฏ€๋กœ matchedGeometryEffect๋ฅผ frame์œ„์— ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

 

Image("Highest in the room")
	.resizable()
	.aspectRatio(contentMode: .fill)
	.clipShape(RoundedRectangle(cornerRadius: 15,style: .continuous))
	.matchedGeometryEffect(id: "image", in:animation)
	.frame(width: 150, height: 150)

GeometryView

 

๋ณธ๊ฒฉ์ ์œผ๋กœ ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

@State ๋ณ€์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ์š”.

 

๋””ํ…Œ์ผ ํ™”๋ฉด์„ ๋ณด์—ฌ์ค„ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•  ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค.

 

    @State var detailShow:Bool = false

 

body ๋ถ€๋ถ„์—” ์•„๋ž˜์™€ ๊ฐ™์ด detailShow๊ฐ€ true๋ผ๋ฉด DetailView๋ฅผ ์•„๋‹ˆ๋ผ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด Image๋ฅผ ๋„์šธ ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

 

๋˜ํ•œ VStack์— onTapGesture๋ฅผ ์ ์šฉํ•˜์—ฌ VStack์„ ๋ˆŒ๋ €์„ ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ detailShow ์ƒํƒœ๊ฐ€ true๋กœ ๋ณ€ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

 

    var body: some View {
        VStack {
            if detailShow {
                DetailView()
            }else {
                Image("Highest in the room")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .clipShape(RoundedRectangle(cornerRadius: 15,style: .continuous))
                    .matchedGeometryEffect(id: "image", in:animation)
                    .frame(width: 150, height: 150)
            }
        }
        .onTapGesture {
            withAnimation {
                detailShow = true
            }
        }
    }

DetailView

 

@ViewBuilder๋ฅผ ์ด์šฉํ•ด DetailView๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๊ณ  Image์˜ ํฌ๊ธฐ์™€ ์œ„์น˜๋ฅผ ์กฐ์ •ํ•ด ์ค๋‹ˆ๋‹ค.

 

Image์˜ ์ขŒ์ธก ์ƒ๋‹จ์— ๋’ค๋กœ ๊ฐ€๊ธฐ ์ด๋ฏธ์ง€๋ฅผ ๋ฐฐ์น˜ํ•˜๊ณ  , ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ๋ˆ„๋ฅด๋ฉด detailShow๊ฐ€ false๊ฐ€ ๋˜๋ฉฐ ์ด์ „ ํ™”๋ฉด์œผ๋กœ ๋˜๋Œ์•„ ๊ฐˆ ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

 

    @ViewBuilder
    func DetailView() -> some View {
        VStack {
            Image("Highest in the room")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .clipShape(RoundedRectangle(cornerRadius: 15,style: .continuous))
                .matchedGeometryEffect(id: "image", in:animation)
                .frame(maxWidth: .infinity, maxHeight: 400)
                .overlay {
                    VStack {
                        Image(systemName: "chevron.left")
                            .font(.title2)
                            .foregroundColor(.black)
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .padding(.top,30)
                            .padding(.leading,20)
                            .opacity(detailShow ? 1 : 0)
                            .onTapGesture {
                                withAnimation {
                                    detailShow = false
                                }
                            }
                        Spacer()
                    }
                }
            
            Spacer()
        }
        .ignoresSafeArea(.all)
    }

Test

 

์•„๋ž˜ GIF์™€ ๊ฐ™์ด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํƒญํ•˜๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋””ํ…Œ์ผ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋˜๊ณ , ๋””ํ…Œ์ผ ํ™”๋ฉด์—์„œ "<"๋ฅผ ๋ˆ„๋ฅด๋ฉด ์ด์ „ ํ™”๋ฉด์œผ๋กœ ๋Œ์•„๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 


์˜ค๋Š˜์€ ์ด๋ ‡๊ฒŒ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ namespace์™€ matchedGeometryEffect๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค.

 

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

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€