[iOS] Custom @Binding DIY 키트로 만들어보는 나만의 바인딩

Youth·2024년 4월 3일
2

TIL

목록 보기
18/21

매주 1개씩은 블로그를 쓰려했지만 저번주에 못써서 살짝 속상한 킴스캐슬입니다
오늘은 새롭게 swiftUI관련한 주제로 찾아뵙게되었습니다 ㅎㅎ

제가 swiftUI에서 그렇게 중요하다는 @State와 @Binding을 공부하다가 코드를 보면서 조금 굳이 이렇게 해야하나...?, 더 좋은 방법이 없을까?를 고민하다가 @Binding의 initalize를 통한 custom에대해서 알게되어서 그부분을 이번 아티클에서 공유해보고자합니다:)

@Binding을 통해서 alert띄우기(개선 전)

만약에 어떤 버튼이 눌렸을 때 네트워크통신을 하고 여러가지 에러중에 하나가 발생했다고 가정해볼까요

여러방법으로 그 에러를 처리할수있겠지만 그 에러를 alert로 띄워준다고 생각해봅시다. 간단하게 코드를 짜보면 아래와같이 짤수있을겁니다

import SwiftUI

let errorTitles = ["1번에러", "2번에러", "3번에러"]

struct ContentView: View {
    @State private var isOn: Bool = false
    @State private var alertTitle: String? = nil
    var body: some View {
        Button {
            // Action
            alertTitle = errorTitles.randomElement()
            isOn.toggle()
        } label: {
            Text("CLICK ME")
        }
        .alert(alertTitle ?? "Error", isPresented: $isOn) {}
    }
}

간단하게 코드를 훑어볼까요

  1. 버튼을 누르면 네트워크통신을 했을때 그 결과로 여러 에러중하나가 발생한걸 그냥 errorTitles중에 랜덤으로 하나의 에러title을 alertTitle에 넣어줍니다

  2. 그리고 isOn이라는 변수를 toggle해줌으로써 true로 바뀌고 .alert의 isPresented가 변화를 감지해 true가 되는순간 alert를 띄워줍니다

  3. 이때 띄워지는 alert의 title은 버튼을 눌렀을때 랜덤으로 들어간 title이 됩니다

@State와 @Binding의 개념을 어렴풋이 알고있어도 위와같은 동작 sequence를 떠올리실수있을겁니다

저는 여기서 한가지 궁금증이 생겼습니다

isPresent의 Binding<Bool>은 어떻게 State의 변화를 감지하는걸까?

지금부터 이 궁금증을 해결해보려 합니다

Binding<Bool>이 변화를 감지하는 방법

import SwiftUI

struct ContentView: View {
    @State private var title = "바꾸기 전"
    var body: some View {
        VStack {
            Text(title)
            CustomRectView(title: $title)
                .padding()
                .background(.red)
        } //:VSTACK
    }
}

struct CustomRectView: View {
    @Binding var title: String
    var body: some View {
        Text(title)
        Button {
            title = "바꾼 후"
        } label: {
            Text("버튼입니다")
        }
    }
}

아마 binding을 한번이라도 공부해보신분들이라면 봤을 @State와 @Binding의 간단한 예시입니다 애플에서도 @State변수앞에 $를 붙이면 Binding으로 바꿔준다고 공식문서에서 설명을 해주고 있습니다

근데 이런생각이들수있죠

$title로 바뀌었다고 하더라도 @State의 변수가 다른뷰에 상태가 공유되는게 뭔가 직관적이지 않아보여요...ㅠㅠ

왜냐면 결국 @State자체도 struct이기때문에 struct라는 value type을 전달해준순간 copy되어서 전달되기때문에 CustomRectView에서 값을 바꿨다고 해도 그값이 ContentView에도 변화가 동일하게 일어나는게 이상하다고 느끼실수도 있습니다

물론 struct임에도 binding의 변화가 state에도 영향을 미치도록 애플이 만든게 binding이긴하지만 순수하게 struct인 value type관점에서보면 이게 좀 찜찜할수가있습니다

그래서 과연 binding은 어떻게 변화를 state에 전달하는지에대해서 알아보겠습니다

우선 바인딩이 어떻게 생겨먹은 친구인지 한번 보면 애플이 추상화밖에 제공해주지 않지만 적어도 Binding을 initalize할때 두개의 클로저를 넣어줘야한다는걸 알 수 있습니다

get과 set입니다
만약에 Binding<String>이라면 Value는 String이 되겠네요
그럼 get에는 문자열를 반환하는 함수가 들어가야겠고
set에는 이 문자열을 가지고 뭔가를 하는 함수가 들어가야겠네요

여기까지가 이 추상메서드를 보고 떠올릴수있는 생각입니다
그리고 이 Value는 당연히 @State변수일겁니다

제가 처음에 이 get과 set으로 이루어진 initalize를 봤을때 아무리봐도 state가 변했을때 이 변화를 감지하는 로직이 어떻게 되는걸까라는 생각이들었습니다

그럼 우리가 애플에서 자동으로 이런기능으로 만들어주는 $를 쓰지말고 직접 binding을 구현해보면 어떨까요?

우선 위 코드에서 get과 set은 어떤상황에서 쓰일지를 생각해봅시다
get은 기본적으로 어떤 값을 가지고 뭔가를 할때 쓰입니다
set은 변수에 다른 값을 넣을때 쓰입니다

우리가 title이라는 변수는
Text에 넣어서 해당 문자열을 UI로 보여줄때 쓰고
Button을 눌렀을때 특정문자열을 title이라는 변수에 넣을때 쓰게됩니다

그러면 Text에 넣을때 get을 쓰고 title에 넣을때 set을 쓰면되겠네요

우선 Binding<String>에 $title대신 Binding을 직접만들어보겠습니다
위 이미지처럼 .init을 통해서 get과 set에 클로저를 정의해주겠습니다

get은 Text에 들어갈 string을 반환해주면되고
set에는 바뀐 string이 들어온다면 그걸 title에 넣어주면됩니다

그러면 만약에 binding으로 선언된 변수의 값을 사용할때는 title의 값이 그대로 return되고 새로운 값을 binding으로 선언된 변수에 넣으면 그 값이 title이라는 @State의 변수에 들어가게됩니다

즉 이렇게 선언을 해놓으면 @Binding이라는 변수가 get되고 set되면 상위뷰, @State를 가지고있는 뷰에서 어떻게 동작할지를 정의할수있게됩니다(closure로 viewModel의 동작을 viewcontroller에서 정의해놓는거랑 비슷한거같네요)

그러면 @Binding이있는 하위뷰의 관점에서 보겠습니다

Text에서 binding변수인 title을 쓰기위해서는 binding을 정의할때 get의 클로저 리턴값인 title자체의 문자열이 반환될겁니다 그래서 이때는 하위뷰의 text에는 바꾸기 전이라는 문자열이 UI에 보여지게될겁니다

그리고 버튼을 누르면 title에 바꾼 후라는 문자열이 들어갈거고 저희가 정의해놓은 set의 클로저가 실행될겁니다 클로저에 바꾼 후라는 newString이 input으로 들어가고 그 문자열이 상위뷰의 @State변수의 title에 할당됩니다

그리고 swiftUI에서는 @State변수의 변화가 생기면 관련뷰를 새로그리는 메커니즘이 존재합니다

결국 아래와같은 순서로 동작하게됩니다

5번이 명령을 내립니다

  1. 뷰를 그려!!!!!

Content뷰를 그립니다

  1. 4번의 title은 바꾸기 전이라는 문자열이 그대로 들어가게됩니다
  2. 3번과 2번의 get set클로저를 가지고있는 Binding을 initalize에 정의해놓은 하위뷰가 생성됩니다

CustomRectView를 그립니다

  1. title은 binding변수이기에 get을 해오는데 get의 결과는 contentview에있는 title이라는 변수의 값이니 바꾸기 전이라는 문자열이 들어갑니다
  2. 버튼을 그립니다(아직 누르지않았으니 동작하지 않습니다)

이제 여기서 버튼을 눌러보겠습니다

버튼을 누릅니다

  1. title에 바꾼 후라는 문자열을 넣어야합니다. 따라서 binding을 만들떄 넣어둔 set클로저를 실행시켜줍니다
  2. 바꾼 후라는 문자열이 newstring이되어서 ContentView의 @State에 들어가게됩니다
  3. @State변수에 변화가 감지되어서 5번이 다시 뷰를 그리라고 명령합니다

이제부터는 위의 순서가 반복되지만 한번만 다시 뷰를 그리는 과정을 나열해보겠습니다

5번이 명령을 내립니다

  1. 뷰를 그려!!!!!

Content뷰를 그립니다

  1. 4번의 title은 바꾼 후이라는 문자열로 바뀌었기때문에 바꾼 후가 Text에 들어갑니다
  2. 3번과 2번의 get set클로저를 가지고있는 Binding을 initalize에 정의해놓은 하위뷰가 생성됩니다

CustomRectView를 그립니다

  1. title은 binding변수이기에 get을 해오는데 get의 결과는 contentview에있는 title이라는 변수의 값이니 바꾼 후라는 문자열이 들어갑니다
  2. 버튼을 그립니다(아직 누르지않았으니 동작하지 않습니다)

결국 우리가 $를 통해서 바인딩을 만들게되면 이와같은 순서로 하위뷰에서의 특정 값을 @State로 넘겨주고 그 변화로인해 view를 redraw하면서 데이터가 바뀐 UI로 변하게되는겁니다

실제로 내부적으로 $를 붙였을때 이렇게 동작하지않을수있지만 결국은 비슷한 방식일거같긴합니다! 이건 제 뇌피셜입니다:)

자 그럼 Binding을 initalize해서 직접 custom해서 만들면 우리가 익숙한방식으로도 동작시킬수있다는걸 알게되었습니다

@Binding을 통해서 alert띄우기(개선 후)

import SwiftUI

let errorTitles = ["1번에러", "2번에러", "3번에러"]

struct ContentView: View {
    @State private var isOn: Bool = false
    @State private var alertTitle: String? = nil
    var body: some View {
        Button {
            // Action
            alertTitle = errorTitles.randomElement()
            isOn.toggle()
        } label: {
            Text("CLICK ME")
        }
        .alert(alertTitle ?? "Error", isPresented: $isOn) {}
    }
}

제가 이 코드에서 불편(?)했던 부분은 알람을 띄울지에 대한 여부를 판단하는 변수와 알람에 어떤 글씨를 띄워줄지를 알려주는 변수두가지가 필요하다는거였습니다

그냥 alertTitle이 nil이 기본이라고 생각하고 에러가 발생했을때 alertTitle에 어떤 값이들어가면 아래와같은 논리가 적용될수있다고 생각했습니다

alertTitle에 어떤 값이 들어갔다면 그 값이 에러메세지란 소리고 에러메세지가 들어갔단소리는 에러가 발생했다는 소리고 에러가발생했다는건 알람을 띄워줘야한다 그러므로 alertTitle만으로도 isOn의 역할을 대체할수있다

즉 우리는 alertTitle의 nil인지 아닌지여부가 isOn을 대체할수있지않을까라는 생각을 해볼수있습니다

그러면 저 $isOn을 customBinding을 통해서 바꿔보면서 get과 set에 적절한 클로저를 넣어주면되겠죠

우선 Binding<Bool>이므로 아래와같이 그림을 그려볼 수 있습니다

  1. get에는 bool을 리턴해주는 클로저
  2. set에는 bool값이 바뀌면 그 값을 @State에 적절하게 전달해주는 클로저

자 우선 get을 볼까요
여기서 bool을 alert을 띄워줄지 안띄워줄지에 관한 변수입니다
그리고 위에서 우리는 alertTitle이 nil인지아닌지로 그 여부를 판단하기로했으니
get에는 { alertTitle != nil }이라는 클로저를 넣으면 될거같습니다
set에는 만약에 alert라는 메서드내부에서 binding변수에 true나 false가 set되면 어떻게할지에대한 클로저를 넣어주면됩니다

근데 사실 alert내부에서는 OK를 눌렀을때 alert을 끄는 기능밖에없기때문에 alert내부의 binding변수가 false가 되는 경우밖에없을겁니다

결국 alert내부의 binding변수가 false가 되면 그 결과가 어떤 @State를 바꾸게되고 view를 새로그리게되는데 새로그린순간에 alertTitle이 nil이 아닌상태이면 { alertTitle != nil } 때문에 alert띄워야한다고 판단해 alert가 없어지지 않을겁니다

따라서 newValue가 false라면 다시 그린 view에서는 alert가 없어져야하고 alert를 없애려면 alertTitle이 nil이되어야하므로 set에는 아래와같은 클로저를 넣어주면됩니다

{ newValue in
    if !newValue {
        alertTitle = nil
    }
}

자그럼 개선시킨 코드를 볼까요

import SwiftUI

let errorTitles = ["1번에러", "2번에러", "3번에러"]

struct ContentView: View {
    @State private var alertTitle: String? = nil
    var body: some View {
        Button {
            // Action
            alertTitle = errorTitles.randomElement()
        } label: {
            Text("CLICK ME")
        }
        .alert(alertTitle ?? "Error", isPresented: .init(get: {
            alertTitle != nil
        }, set: { newValue in
            if !newValue {
                alertTitle = nil
            }
        })) {}
    }
}

버튼을 눌렀을때 alertTitle에 예를들어 1번 에러가 들어갔을때 @State가 바뀌니까 View를 새로그리고 alert를 그릴지에 대한 get의 결과가 alertTitle이 nil이 아니므로 true라서 alert가 보이게될겁니다

alert의 닫기버튼을 누르면 내부에서 Binding변수에 false라는 값을 set해줄거고 set클로저의 newValue에 false가 들어가는데 newValue가 false이므로 alertTitle에 nil이들어가고 @State가 값을 감지해서 view를 새로그리게됩니다. 새로그리면 alertTitle가 nil이니 binding의 get결과가 false가되어서 alert가 사라지게될겁니다

이런식으로 바꿔볼수도있을것같습니다 ㅎㅎ

@State @Binding은 정말 중요한개념이고 자주쓰이는 wrapper인데 @State는 동작이 직관적인 반면에 제기준에서 @Binding의 동작은 그렇게 직관적이지 않아서 이것저것 알아보다보니 custom해서 binding을 만들수있다는걸 알게되어서 이렇게 정리까지 해보게되었습니다 ㅎㅎ

아마 이 글을 이해하신다면 binding을 이전보다는 조금 더 자유자재로 쓰실수있지않을까 그런생각이드네요:)


마무리(갑자기 스유??)

저는 UIkit을 지금까지 주로 써왔고 swiftUI는 아직 시기상조다...라는 생각이 매우강한 사람중에 한명이었는데요. 이번에 새로운 프로젝트를 시작하면서 한번 스유로해볼까?라는 생각이들더라고요

늘 마음속으로는 UIkit도 제대로 못하면서 swiftUI를 해도될까?, 지금 스유를 하는게 취업에 도움이될까?라는 생각에 사로잡혀서 도전하지못했었거든요

근데 한두달전부터 모든 공부나 개발의 목적이 취업이되기시작하면서 슬럼프라고해야할까요. 그런게 왔던거같아요. 저는 개발하고 뭔가를 만드는 그 과정을 좋아했고 즐겼는데 모든 action의 목적자체가 취업이되는 순간 개발이 재미있지 않았던? 그런느낌이었다랄까요

그래서 아직 학교를 1년 더 다녀야하고 그럼 취업까진 시간이남았으니 너무 취업에 목메지말자 내가 당장 해보고싶은걸 하자라는 생각이들어서 swiftUI를 공부하기 시작했습니다

UIkit을 공부하던것처럼 궁금한게있으면 최대한 깊게 공부를 하고 대체 왜 이런걸까? 원리가뭘까?를 생각하다보니 이번에는 @Binding에 대해서 이렇게 공부를 해봤던것같습니다

물론 여전히 UIkit으로 진행되고있는 프로젝트도 있기에 아티클의 주제가 swiftUI도 하나 늘었다고 생각해주시면 될것같습니다:)

그럼 다음에도 흥미로운 주제로 돌아오겠습니다! 그럼 20000!

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

3개의 댓글

comment-user-thumbnail
2024년 4월 5일

Binding이 저한테는 아직 생소한 개념이기는한데, 이미지를 곁들여서 코드 흐름을 잘 설명해주셨네요 ! @Binding 덕분에 하나 배워갑니다 ㅎ

답글 달기
comment-user-thumbnail
2024년 4월 5일

점차 마법처럼 알아서 해주지만 코드 흐름이 한 눈에 잘 안들어오는 것 같아요
그럴수록 내부 동작을 올바르게 이해하는 게 중요해지는 것 같습니다

Binding 객체는 escaping 클로저를 통해 작동하는 군요
애노테이션이 아닌 생성자를 통해 직접 Binding 객체를 만드는 게 흥미롭습니다

isOn 객체 한 줄이 사라지고 5-6줄이 늘어났는데 이득보다 손해가 조금 더 큰 느낌이기도 합니다

1개의 답글