본문 바로가기

IOS Swift

멀티윈도우 지원하는 앱 만들기 - iOS

Multiple windows 지원하는 앱 만들기

생각보다 무척 간단합니다. (코드로 커스터마이징 하는 건 좀 복잡하지만 ㅎㅎ)

프로젝트 설정에서 Supports multiple windows 체크하면 됩니다.

앱을 실행시키고 위의 + 버튼을 누르면 UIScene이 한개가 더 생성됩니다.

멀티윈도우 만들기 진짜 간단하죠? 🥰

 

그런데 아이패드에서 맨날 저 버튼을 눌러서 창을 한개 더 만드는건 너무 불편하잖아요?

그래서 사용자가 편하도록 위 앱의 사진 하나를 드래그 해서 좌우측 끝에 드롭하면 창을 하나 더 만들도록 해보겠습니다.

 

완성본

완성본

 


UI 만들기

우선 UI를 먼저 완성 시키고 가겠습니다.

아래 그림처럼 스토리보드를 만들어 주세요.

 

그 다음 CollectionView Cell을 만들어 줘야겠죠? 저는 xib을 이용해서 만들어 줬습니다.

 

Resource 준비

https://developer.apple.com/documentation/uikit/app_and_environment/scenes/supporting_multiple_windows_on_ipad

 

Apple Developer Documentation

 

developer.apple.com

 

애플에서 제공하는 예제 다운로드 받으셔서 image file들 다운받아서 사용하겠습니다.

여기에 나와있는 코드들 또한 애플 예제를 보고 따라한 것 입니다.

따라해 보면서 겪었던 문제상황들을 추가했습니다.

 

Model

import UIKit

public struct Photo {
    
    static let GalleryOpenDetailActivityType = "VCKey"
    static let GalleryOpenDetailPath = "openDetail"
    static let GalleryOpenDetailPhotoIdKey = "photoId"
    
    let name: String
    
    var openDetailUserActivity: NSUserActivity {
        // Create an NSUserActivity from our photo model.
        // Note: The activityType string below must be included in your Info.plist file under the `NSUserActivityTypes` array.
        // More info: https://developer.apple.com/documentation/foundation/nsuseractivity
         //NSUserActivity란 사용자가 사용하던 앱의 상태를 저장 혹은 복원하기 위한 객체입니다.
        let userActivity = NSUserActivity(activityType: Photo.GalleryOpenDetailActivityType)
        userActivity.title = Photo.GalleryOpenDetailPath
        userActivity.userInfo = [Photo.GalleryOpenDetailPhotoIdKey: name]
        return userActivity
    }

}

struct PhotoSection {
    let name: String
    let photos: [Photo]
}

struct PhotoManager {
    static let shared = PhotoManager()
    
    let sections: [PhotoSection] = [
        PhotoSection(name: "Section 1", photos: [
            Photo(name: "1.jpg"),
            Photo(name: "2.jpg")
        ]),
        PhotoSection(name: "Section 2", photos: [
            Photo(name: "3.jpg"),
            Photo(name: "4.jpg"),
            Photo(name: "5.jpg")
        ]),
        PhotoSection(name: "Section 3", photos: [
            Photo(name: "6.jpg"),
            Photo(name: "7.jpg")
        ])
    ]
}

 

DragAndDrop

드드 하기위해 CollectionView에 DragDelegate를 만들었습니다.

extension ViewController: UICollectionViewDragDelegate {
    
    func photo(at indexPath: IndexPath) -> Photo {
        return photoSections[indexPath.section].photos[indexPath.row]
    }
    
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        var dragItems = [UIDragItem]()
        let selectedPhoto = photo(at: indexPath)
        if let imageToDrag = UIImage(named: selectedPhoto.name) {
            //NSUserActivity란 사용자가 사용하던 앱의 상태를 저장 혹은 복원하기 위한 객체입니다.
            let userActivity = selectedPhoto.openDetailUserActivity
            //itempProvider는 끌어서 놓기 활동 중에 프로세스 간에 공유되는 데이터 또는 파일을 전달합니다.
            let itemProvider = NSItemProvider(object: imageToDrag)
            itemProvider.registerObject(userActivity, visibility: .all)

            let dragItem = UIDragItem(itemProvider: itemProvider)
            dragItem.localObject = selectedPhoto
            dragItems.append(dragItem)
        }
        return dragItems
    }
}

 

DataSourceDelegate 

class ViewController: UIViewController {
    let photoSections = PhotoManager.shared.sections
    @IBOutlet weak var myCollectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        myCollectionView.dataSource = self
        myCollectionView.dragDelegate = self
        myCollectionView.delegate = self
        myCollectionView.register(UINib(nibName: "MyCollectionViewCell", bundle: nil),
                                       forCellWithReuseIdentifier: "Cell")
    }
    
    //코드로 Scene 생성, Scene 제거 테스트해보기 위해 임의 버튼 생성해서 해봄
    @IBAction func removeSession(_ sender: Any) {
        //NSUserActivity란 사용자가 사용하던 앱의 상태를 저장 혹은 복원하기 위한 객체입니다.
        let activity = NSUserActivity(activityType: Photo.GalleryOpenDetailActivityType)
        activity.title = "openDetail"
        activity.userInfo?["photoId"] = "1.jpg"
        
        //Session연결 - Scene 생성
        UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil)
        
        //Session연결 제거 - Scene 제거
        guard let sceneSession = self.view.window?.windowScene?.session else { return }
        UIApplication.shared.requestSceneSessionDestruction(sceneSession, options: nil, errorHandler: nil)
    }
}

extension ViewController: UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let selectedPhoto = photo(at: indexPath)
        if let detailViewController = PhotoDetailViewController.loadFromStoryboard() {
            detailViewController.photo = selectedPhoto
            navigationController?.pushViewController(detailViewController, animated: true)
        }
    }

}

extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return photoSections[section].photos.count
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return photoSections.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        
        if let cell = cell as? MyCollectionViewCell {
            let selectedPhoto = photo(at: indexPath)
            cell.image = UIImage(named: selectedPhoto.name)
        }
        return cell
    }
}

DelegateFlowLayout

import UIKit

private let itemsPerRow: CGFloat = 5
private let spacing: CGFloat = 20

extension GalleryViewController: UICollectionViewDelegateFlowLayout {
    
    public func collectionView(_ collectionView: UICollectionView,
                               layout collectionViewLayout: UICollectionViewLayout,
                               sizeForItemAt indexPath: IndexPath) -> CGSize {
        let totalSpacing = (2 * spacing) + ((itemsPerRow - 1) * spacing)
        let width = (collectionView.bounds.width - totalSpacing) / itemsPerRow
        return CGSize(width: width, height: width)
    }
    
    public func collectionView(_ collectionView: UICollectionView,
                               layout collectionViewLayout: UICollectionViewLayout,
                               insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
    }
    
    public func collectionView(_ collectionView: UICollectionView,
                               layout collectionViewLayout: UICollectionViewLayout,
                               minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return spacing
    }
    
    public func collectionView(_ collectionView: UICollectionView,
                               layout collectionViewLayout: UICollectionViewLayout,
                               minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return spacing
    }
    
}

 

Info.plist

info.plist에 NSUserActivityTypes 추가하고  Item에 "VCKey"으로 생성해줍니다.

애플 예제 Model에 이거 생성해주라고 얘기가 나와있었는데 못보고 한참 해맸습니다.. 😱

이제 거의 다 했습니다!

Drag할때 NSUserActivity로 앱의 상태를 저장 했으니 다시 복원해야겠죠?

 

SceneDelegate

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
 
    // MARK: - UIWindowSceneDelegate
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            if !configure(window: window, with: userActivity) {
                Swift.debugPrint("Failed to restore from \(userActivity)")
            }
        }
    }
    
    //사용자가 종료 후 앱을 다시 실행하면 이전에 보고 있던 앱의 화면이 뜸
    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        return scene.userActivity
    }

    //userActivity를 복원하는 과정
    func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
        var configured = false

        if activity.title == Photo.GalleryOpenDetailPath {
            if let photoID = activity.userInfo?[Photo.GalleryOpenDetailPhotoIdKey] as? String {
                // Restore the view controller with the photoID.
                if let photoDetailViewController = PhotoDetailViewController.loadFromStoryboard() {
                    photoDetailViewController.photo = Photo(name: photoID)
                    
                    if let navigationController = window?.rootViewController as? UINavigationController {
                        navigationController.pushViewController(photoDetailViewController, animated: false)
                        configured = true
       
                 
                    }
                }
            }
        }
        return configured
    }
}

stateRestorationActivity 이거는 처음 알았네요. 백그라운드에서 메모리 모자르면 앱을 강제로 종료시켜버린다는 것 까지는 알고 있었는데 그것에 대비하기 위한 기술이 있다는 것을 알았습니다!

물론 여기서는 깊게 다루지 않고 넘어가겠습니다. ㅎ_ㅎ 👻

 

이제 다 했으니 실행한번 해볼까요?

오잉? 새로운 Scene이 만들어지긴 하는데 왜 멀티태스킹이 안되고 Full Screen으로 고정이 되버리는 거죠....? 

 

MultiTasking

iOS 9부터 아이패드 멀티태스킹이 지원됩니다.

하나의 화면에 slide over 또는 split view로 두가지 앱을 동시에 띄울 수 있습니다.

 

MultiTasking 지원하기 위한 조건

일단 아래의 Requires full screen 체크 해제되어 있어야 합니다.

물론 자신의 앱이 멀티태스킹 하는 것을 막겠다! 하면 체크하시면 됩니다.

You have to modify your project to support multitasking. According to WWDC 2015 video, to adopt your app for multitasking, satisfy these requirements:

  1. Build your app with iOS 9 SDK
  2. Support all orientations (저는 이문제 였습니다!.)
  3. Use Launch Storyboards

 

해결

Supported interface orientations (iPad)에 원래 3 items 였는데 모든 방향을 지원해야 멀티태스킹 된다고 해서

나머지 한방향도 추가해주었습니다.

완성~!

완성본

 

 

출처 및 참고

멀티윈도우 Apple 예제 : https://developer.apple.com/documentation/uikit/app_and_environment/scenes/supporting_multiple_windows_on_ipad

멀티윈도우 : https://icksw.tistory.com/137

멀티윈도우 : https://darjeelingsteve.com/articles/Advanced-Multi-window-UIs-on-iPadOS-with-Drag-and-Drop-and-State-Restoration.html

멀티태스킹 : https://newbedev.com/is-it-possible-to-opt-your-ipad-app-out-of-multitasking-on-ios-9

반응형