iOS 9 custom URL scheme

Category
HTML/CSS/JS, 개발 노트
Posted
2015-12-17 09:38
세월호 참사 1주기 결코 잊지 않겠습니다.

최근 iOS 9.x 버전으로 올라오면서 그 동안 잘 동작하던 custom URL scheme에 빨간 불이 들어왔다.
기존의 iframe 방식으로 처리하던 것이 갑자기 먹통이 되어버린 상황이 똬악… = _=a

iOS 10.3.0, 10.3.1 에서 추가 이슈가 발견되어 관련 이슈를 http://blog.publisher.name/1524에 포스팅 해두었으니 아래 내용을 참고하시는 분들은 함께 보시기 바랍니다.

급하게 해결책을 찾아보니 역시 우리의 stackoverflow께서는 해결책을 갖고 계셨고, 자동으로 앱 미설치시에는 자동으로 앱스토어로 보내야 했기에 다음과 같은 코드를 사용하여 처리했다.

custom URL scheme 처리 코드
$(document).on('click.callApp', '... a', function(event){
	event = event || window.event;
	event.preventDefault ? event.preventDefault() : event.returnValue = false;

	var ua = navigator.userAgent.toLowerCase();
	var url = $(this).attr('href');
	var isIphone = /iphone|ipad/.test(ua);
	var isAndroid = /android/.test(ua);
	var isChrome = /chrome/.test(ua);
	var appStoreAppName = isIphone ? .....;
	var appScheme = isChrome ? ..... ;

	function runApp(appCustomScheme, appStoreAppName){
		var clickedAt = +new Date;
		var appCheckTimer = setTimeout(function(){
			if (+new Date - clickedAt < 2000){
				window.location.href = appStoreAppName;
			}
		}, 100);
		window.location.href = appCustomScheme;
	}

	runApp(appScheme, appStoreAppName);
});

이렇게 해서 문제가 해결 된 줄 알고 한 동안 문제없이 잘 있었는데.... 그런데.. ㅠ_ㅠ
그제 iOS 9.2로 업그레이드를 하고 나서 또 다시 문제가 터졌다....
기존에는 문제가 없었으나 위 코드를 걸 경우, 사용자의 컨펌을 기다리지 않고 무조건 앱스토어로 넘어가는 일이 발생해버렸...

몇 번의 테스트 끝에 iOS 9.2 에서의 규칙(?)을 발견했다. (아마 iOS 버그가 아닐까 싶기는 하다.)
기본적으로 window.location 을 변경하는 것은 여전히 동일한데, 상황에 따른 동작이 달랐으니,

  • click 이벤트 핸들러 내에서 custom URL scheme 호출에 의해 confirm box가 띄워졌을 때에는 브라우저가 사용자 입력을 기다리지 않는다.
  • page load 시에 custom URL scheme 호출에 의해 confirm box가 띄워졌을 때에는 브라우저가 사용자의 입력을 기다린다.

이 맷돌에 그게 없는 상황 때문에 멘붕과 삽질을 계속.... 하아...
특히 전자의 케이스가 설마 click 이벤트 핸들러 차이로 인해서 그런 현상이 발생 될 거라고는 전혀 예상을 못했기에 거진 1시간 가량을 미궁 속으로 카오스 오브 혼돈을 경험....

무튼, 9.x 에서의 이러한 상황 때문에 결국 9.x 이상은 page load시에 처리 할 수 있도록 별도의 페이지를 만들고, 그 이하 버전에서는 기존 방식을 차용하여 결국 아래 코드로 일단락 지었다.

갱신된 custom URL scheme 처리 코드 on click event
$(document).on('click.callApp', '... a', function(event){
	event = event || window.event;
	event.preventDefault ? event.preventDefault() : event.returnValue = false;

	var ua = navigator.userAgent.toLowerCase();
	var url = $(this).attr('href');
	var isIos = /iphone|ipad/.test(ua);
	var iOsVersion = isIos ? parseFloat(ua.substr(ua.search(/ipad|iphone/), 30).match(/\d+\_+\d+/)[0].replace('_', '.')) : null;
	var isAndroid = /android/.test(ua);
	var isChrome = /chrome/.test(ua);
	var appStoreAppName = isIos ? ..... ;
	var appScheme = isChrome ? ..... ;

	function runApp(appCustomScheme, appStoreAppName){
		var clickedAt = +new Date;
		var appCheckTimer = setTimeout(function(){
			if (+new Date - clickedAt < 2000){
				window.location.href = appStoreAppName;
			}
		}, 100);
		window.location.href = appCustomScheme;
	}
	if(isIos && iOsVersion >= 9){
		window.open('http://.../index.html?' + encodeURIComponent( ... ));
	}else{
		runApp(appScheme, appStoreAppName);
	}
});
갱신된 custom URL scheme 처리 코드 on page load
<head>
	....
	<script>
		var ua = navigator.userAgent.toLowerCase();
		var url = location.search.replace('?', '');
		var isIos = /iphone|ipad/.test(ua);
		var iOsVersion = isIos ? parseFloat(ua.substr(ua.search(/ipad|iphone/), 30).match(/d+_+d+/)[0].replace('_', '.')) : null;
		var isAndroid = /android/.test(ua);
		var isChrome = /chrome/.test(ua);
		var appStoreAppName = isIos ? ..... ;
		var appScheme = isChrome ? ..... ;
	
		function runApp(appCustomScheme, appStoreAppName){
			var clickedAt = +new Date;
			var appCheckTimer = setTimeout(function(){
				if (+new Date - clickedAt < 2000){
					window.location.href = appStoreAppName;
				}
			}, 100);
			window.location.href = appCustomScheme;
		}
		if(isIos && iOsVersion >= 9){
			window.open('http://.../index.html?' + encodeURIComponent( ... ));
		}else{
			runApp(appScheme, appStoreAppName);
		}
	});
	</script>
	...
</head>

iOS user agnet string을 까보니... iOS의 버전 정보가 9_2 이런식으로 되어 있더라... = _=a
얘넨 또 왜 요따구인지... 그냥 편하게 숫자로 하라고 ㅠ_ㅠ
어쨌든 이렇게 처리 하고나니, 일단 iOS 9 이상 그리고 그 아래 버전에 대한 처리에 문제가 없어 보인다.

iOS 9.2, 이미지 왼쪽 : 앱이 설치되어 있는 경우, 이미지 오른쪽 : 앱 미설치 된 경우

그런데 적용을 해 놓고 테스트를 하다 보니, 재밌는(?) 상황이
이전에는 새 창을 띄워 해당 페이지에서 custom URL scheme 처리를 할 때에는 해당 페이지가 남아있어서 불편한게 있었는데 이제는 새 창에서 처리할 때 취소를 누르든 확인을 누르든 알아서 해당 페이지를 닫아버리고 앱스토어로 보내거나 앱을 실행시키거나 하니 어떤 면에서는 또 요곤 또 잘했네 싶다 ㅋㅋㅋ
추측하건데, 애플이 의도한 바는 custom URL scheme 처리를 새 창으로 띄워서 처리하도록 하는 것인듯 하다.
기존에 많은 앱들이 빈 페이지를 띄워 여기서 custom URL scheme 처리를 했는데 이게 사파리 상에 남아서 나중에 다시 사파리를 열 때 이걸로 인해 또 앱이 실행되는 것이 여간 불편한 것이 아니었기 때문에 이 문제가 반영된 듯 보인다. 다만, click event handler에 대한 문제는 버그인듯.... = _=a

그런데... 다음 iOS 업데이트 버전에서는 또 처리 형태가 바뀌는건 아니겠지? ㄷㄷㄷ;
요새 애플이 universal link인가 뭔가를 미는거 같던데.... 안드로이드 파편화만으로도 충분히 힘들다고 ㅠㅠ

PS. 이 자릴 빌어 원인을 찾는데 도움을 주신 동욱팀장님께 감사를~~~ (절대 언급해 달라고 하셔서 쓰는 건 아님 ㅋㅋㅋㅋ 팀장님 I♡U )


포스팅 후 다른 테스트를 진행하다가 iPhone iOS 9.1에서 또 다른 버그(아마도 버그일듯한)가 발견되었다.
iPad Mini iOS 9.1에서는 상기 코드로 동작이 잘 되는데, iPhone iOS 9.1에서는 window.open으로 호출되는 창이 무한로딩이 되어버리는 현상이 발견되었다.
하아... 이제 하다하다 iOS마저도 단말기별 버그가 발생하나보다.

몇 번의 테스트를 걸쳐 결국 9이하에서는 기존 방식 대로, 9 ~ 9.2 사이에서는 페이지 이동 방식으로, 9.2에서는 새창을 여는 방식으로 변경...
도대체 왜 iPhone 조차도 이 모양 이 꼴이 되었냐...

아래는 iOS 9.1 버그에 대응한 코드

갱신된 custom URL scheme 처리 코드 on click event (iOS 9.1 bug 대응)
$(document).on('click.callApp', '... a', function(event){
		event = event || window.event;
		event.preventDefault ? event.preventDefault() : event.returnValue = false;
	
		var ua = navigator.userAgent.toLowerCase();
		var url = $(this).attr('href');
		var isIos = /iphone|ipad/.test(ua);
		var iOsVersion = isIos ? parseFloat(ua.substr(ua.search(/ipad|iphone/), 30).match(/d+_+d+/)[0].replace('_', '.')) : null;
		var isAndroid = /android/.test(ua);
		var isChrome = /chrome/.test(ua);
		var appStoreAppName = isIos ? ..... ;
		var appScheme = isChrome ? ..... ;
	
		function runApp(appCustomScheme, appStoreAppName){
			var clickedAt = +new Date;
			var appCheckTimer = setTimeout(function(){
				if (+new Date - clickedAt < 2000){
					window.location.href = appStoreAppName;
				}
			}, 100);
			window.location.href = appCustomScheme;
		}
		if(isIos && iOsVersion >= 9.2){
			window.open('http://.../index.html?' + encodeURIComponent(url));
		}else if(isIos && iOsVersion >= 9){		
			window.location.href = 'http://.../index.html?' + encodeURIComponent(url);
		}else{
			runApp(appScheme, appStoreAppName);
		}
	});
Authored By 멀더끙