[iOS] - 앱에 URL Sceheme 적용하기



앱에 URL Scheme을 적용해보자.


앱에 URL Scheme을 적용한다는 것은 특정 형태의 URL로 내 앱을 호출할 수 있다는 말이다. Scheme이라는 것은 url에서 https://~~http에 해당한다. 내 앱의 Scheme이 예를 들어 hello라면, 사파리같은 곳에서 hello://~~/~~이런 형태로 url을 입력했을 때 OS에서 해당 scheme를 가지고 실행할 수 있는 앱을 찾아서 실행시키는 것이다.

개요

커스텀 url scheme는 앱 안에 있는 자원에 대해 참조할 수 있는 방법을 제공한다. 사용자는 예를 들어 이메일 안에 있는 커스텀 url을 클릭해서 내 앱의 특정 기능을 실행시킬 수 있다.

다른 앱 또한 특정한 컨텍스트 데이터를 가지고 내 앱을 실행 가능하다.

커스텀 URL scheme가 deep linking의 형태를 띄고 있으므로, universal link를 사용하는 것이 좋다. 애플은 일반적인 시스템 앱의 scheme를 가지고 있다. mailto, tel, sms, facetime 같은 것들이 있다.

주의

URL Sceheme는 앱에 공격 벡터(해커가 컴퓨터나 네트워크에 접근하기 위해 사용하는 경로/방법)를 제공할 수 있으므로, 모든 URL 파라미터가 유효한지 검사하고 유효하지 않은 URL들은 차단해야 한다. 추가로, 사용자의 데이터를 손상시키지 않는 한에서의 행동들만 허용해라. 예를 들어, 다른 앱이 사용자에 대한 민감 정보에 접근하거나 내용을 지우게 하면 안된다. 코드를 이용해서 URL을 처리할 때, 테스트 케이스가 적절하지 않은 URL에 대해서도 테스트할 수 있게 해야 한다.

커스텀 URL Scheme을 지원하려면

  1. 앱의 URL의 형태를 정의한다.
  2. Scheme를 등록해서 시스템이 적절한 URL을 내 앱으로 연결시킬 수 있게 해야 한다.
  3. 앱이 받는 URL을 처리한다.

URL은 무조건 커스텀 scheme 이름으로 시작해야 한다. 앱이 지원하는 옵션에 대한 파라미터를 추가할 수 있다. 예를 들어 이름과 사진을 보여줄 인덱스를 포함하는 URL을 받는 사진 앱이 있다고 해보자. 그러면 아래와 같은 URL이 들어올 수 있다.

myphotoapp:albumname?name="albumname"
myphotoapp:albumname?index=1

클라이언트는 scheme를 기반으로 한 URL로 앱의 UIApplicationopen(_:options:completionHandler:) 메서드를 실행시켜서 앱을 열어달라고 요청한다. 클라이언트는 앱이 URL을 열 때 시스템한테 클라이언트에 알려달라고 요청할 수 있다.

let url = URL(string: "myphotoapp:Vacation?index=1")

UIApplication.shared.open(url!) { (result) in
    if result {
       // The URL was delivered successfully!
    }
}

URL Scheme 등록하기

URL scheme 등록은 어떤 URL이 앱을 열지를 명시하는 것이다. Xcode의 프로젝트 설정의 Info 탭에서 scheme를 등록한다.

image

  • URL Scheme 부분에는 URL에 사용할 접두어를 정의한다.
  • 앱의 역할을 선택한다
  • 앱의 identifier를 정의한다.

제공한 identifier는 같은 scheme를 가지는 다른 앱들과 내 앱을 구별할 수 있게 해준다. 유일성을 보장하기 위해서, 회사 도메인과 앱 이름을 함친 DNS 문장을 명시한다. (자유긴 하다.) 어떤 URL scheme는 시스템적인 측면에서 앞 뒤가 바뀌게 된다. 시스템은 잘 알려진 URL을 일치하는 시스템 앱으로 연결하고 http 기반의 잘 알려진 URL을 특정 앱으로 연결한다.(유튜브같은 것) 참고록 애플이 지원하는 scheme는 여기 서 확인이 가능하다.

URL 입력 들어온 것 처리하기

다른 앱이 내 커스텀 scheme를 포함한 URL을 열면, 시스템은 내 앱을 실행시키고 필요할 경우 맨 앞의 화면에 노출시킨다. 시스템은 앱 delegate의 application(_:open:options) 메서드를 호출해서 URL을 앱에 전달한다. URL의 내용을 파싱하고 적절한 행동을 취하게 하는 코드를 추가한다. URL이 제대로 파싱되었는지 보장하기 위해, NSURLComponents API를 사용해서 url 내의 요소들을 추출하자. 내 앱을 실행시킨 다른 앱에 대한 정보와 같이 추가적인 정보를 얻으려면 시스템에서 제공된 options 딕셔너리를 이용한다.

func application(_ application: UIApplication,
                 open url: URL,
                 options: [UIApplicationOpenURLOptionsKey : Any] = [:] ) -> Bool {

    // Determine who sent the URL.
    let sendingAppID = options[.sourceApplication]
    print("source application = \(sendingAppID ?? "Unknown")")

    // Process the URL.
    guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
        let albumPath = components.path,
        let params = components.queryItems else {
            print("Invalid URL or album path missing")
            return false
    }

    if let photoIndex = params.first(where: { $0.name == "index" })?.value {
        print("albumPath = \(albumPath)")
        print("photoIndex = \(photoIndex)")
        return true
    } else {
        print("Photo index missing")
        return false
    }
}

시스템은 또한 앱이 지원하는 커스텀 파일 타입을 열기 위해 앱 delegate의 application(_:open:options)를 사용한다.

앱이 Scenes에 맞춰져 있고 앱이 실행되지 않은 상태에서는 시스템은 URL을 실행 이후에 URL을 scene(_:willConnectTo:options delegate 메서드에 전달하고, 앱이 메모리에서 실행되고 있거나 suspended된 상태에서 URL을 열 때는 scene(_:openURLContexts:)에 전달한다.

func scene(_ scene: UIScene, 
           willConnectTo session: UISceneSession, 
           options connectionOptions: UIScene.ConnectionOptions) {

    // Determine who sent the URL.
    if let urlContext = connectionOptions.urlContexts.first {

        let sendingAppID = urlContext.options.sourceApplication
        let url = urlContext.url
        print("source application = \(sendingAppID ?? "Unknown")")
        print("url = \(url)")

        // Process the URL similarly to the UIApplicationDelegate example.
    }

    /*
     *
     */
}

적용해보기

위에까지는 개념이었고, 내 프로젝트에 직접 적용해보았다. 우선 위에서도 언급한 것과 같이 아래처럼 내 앱의 url Scheme을 적용한다. 프로젝트 설정 > info 탭 > URL Types 에서 추가할 수 있다.

image

위와 같이 설정하면 내 앱의 scheme은 iosgiftshopchannel가 된다. Scheme을 iosgiftshopchannel로 설정했다는 것은 아이폰에서 사파리의 주소창에 iosgiftshopchannel://~~어쩌구와 같이 주소를 입력하면 os에서 iosgiftshopchannel라는 scheme을 확인하고 이로 실행가능한 앱이 있는지 찾아준다.

그래서 여기까지 설정하고 사파리에 iosgiftshopchannel://hello와 같이 치면(hello부분에는 어떤 주소를 넣어도 상관없다.) 아래와 같은 화면이 뜨며 내 앱으로 이동하는 것을 확인할 수 있다.

image

그런데 단순히 외부에서 내 앱을 URL Scheme으로 호출만 하기 위해 사용하지는 않는다. 특정 url로 내 앱을 호출했을 때, scheme 뒤에 나오는 url을 parsing하여 또 앱에서 주소별로 다르게 동작하도록 설정할 수 있다. 외부에서(사파리같은 곳) URL Scheme을 호출했을 때 이를 처리하는 부분은 위에서도 나왔지만 SceneDelegate를 사용한다는 가정 하에 func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)라는 메서드에서 처리해준다. 위에서는 scene(_:willConnectTo:options에서도 처리해준다고 했지만 내가 테스트했을 때는 외부에서 URL Scheme을 호출하면 앞의 메서드가 호출되었다.

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) 메서드를 아래와 같이 구현해주면 URL Scheme으로 호출할 때 주소가 parsing 되어서 나온다.

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    for context in URLContexts {
        print("url: \(context.url.absoluteURL)")
        print("scheme: \(context.url.scheme)")
        print("host: \(context.url.host)")
        print("path: \(context.url.path)")
        print("components: \(context.url.pathComponents)")
      }
}

iosgiftshopchannel://hello/my/name/is?name=3의 url로 앱을 호출했을 때 출력 결과는 아래와 같이 나온다.

image

지금은 url을 출력만 했지만, 여기서 url에 따라 원하는 작업을 하면 된다.