iOS Tutorial: How to integrate multiple windows feature (introduced in iOS13) in iPadOS

ipad-os-ios13

From this iOS tutorial, you will learn:

  • Different features integrated into iPadOS
  • What multiple windows on the iPad is
  • The types of windows 
  • The benefits of using this feature
  • What types of apps can benefit from this feature
  • Steps to integrate multiple windows feature in iPadOS

iOS 13 was launched with a lot of new features and functionalities. These advancements were not only for iPhoneOS but also for iPadOS. In this blog, we will talk about one of the most important features that recently launched- multiple windows. Herein we will talk about how to integrate multiple windows feature in iPadOS.

With the release of iOS13, iPad has come nearer to functioning as the main computer. A lot of new features released in iOS13 have made iPad Pro a great MacBook replacement. Let us take a look at the new features that have accomplished this feat.

  1. Desktop Class Safari
  2. Multiple App Instances (Windows) At The Same Time
  3. Safari Download Manager 
  4. External Devices Support in Files
  5. Local Storage Management in Files
  6. Better Text Manipulation
  7. Automation with Shortcuts
  8. Mouse Support
  9. Better Home Screen
  10. Dark Mode

The main feature that has led to this advancement is using multiple instances (or windows) for single or multiple apps at the same time. 

What are the Multiple Windows on the iPad?

Previously, there was a feature that lets the users have multiple tabs of different apps on their iPad but multiple windows for the same or different applications were introduced in iOS 13. 

This feature enables your app to run two instances of your interface side-by-side. In simple words, if it is a document-based app, people could have multiple document windows open at the same time. In simpler words, your users will love it.

Bonus: Multiple windows are created easily by using simple features of drag and drop. 

What are the Types of Windows?

  1. Primary window: It contains multiple app objects and the actions associated with them. People tend to interact with a primary window over time. 
  2. Auxiliary window: It contains a single object and the actions associated with it. People tend to interact with an auxiliary window only once before closing it. 

What are the Benefits of Multiple Windows?

  • Multiple windows show different areas of the content. For instance, people might have one primary Mail window to display their Inbox and another to show their Drafts mailbox.
  • Auxiliary windows also give users additional views into the app’s content and functionality. 
  • Users are enabled to act in one window and refer to something in the other window.

Which types of apps utilize this feature?

Most apps can utilize this feature in some way or another. Yet, it should be made sure that this feature is not mandatory for the functioning of your app. This feature is only to improve the multitasking feature of iPadOS and enhance user-experience.

Some examples of apps that utilize this feature in an appropriate manner.

  • Document-based apps
  • Navigation based apps (Maps)
  • Web browsing apps (Safari)
  • Dates/ Event management apps (Calendar)

Let us now see the steps of integrating multiple windows feature in iPadOS 

Steps to Integrate Multiple Windows Feature in iPadOS

  1. Create a new project in Xcode

    integrate multiple windows

  2. Create a Single View Application

    integrate multiple windows

  3. Enter the project name (For instance, SOChatDemo)

    integrate multiple windows

  4. Create a “MainSplitViewController” class of UISplitViewController

    SS4

  5. Add and SplitViewController in Main.storyboard and assign MainSplitViewController class to it.

    integrate multiple windows

    integrate multiple windows

  6. Drag and drop “Common”, “Model”, and “View” folder in the app (from demo app) because it is required for chat data (here, we are showing offline chat data)

    integrate multiple windows

  7. dd ChatListViewController class to show the Chat User list.

    Get a UIViewController in main.storyboard and assign ChatListViewController class to it.

    integrate multiple windows

  8. Take IBOutlet of UITableview and declare an array of User’s Chat list from Model => MessageModel class (UserModel)

    class ChatListViewController: UIViewController {
    
    //MARK:- Variables
    var arrUserList : [UsersModel] = []
    
    //MARK:- IBOutlets
    @IBOutlet weak var tblUsers: UITableView!
    
    //MARK:- UIView Life Cycle
    override func viewDidLoad() {
    super.viewDidLoad()
    arrUserList = generateRandomUsers()
    tblUsers.estimatedRowHeight = 76
    tblUsers.rowHeight = UITableView.automaticDimension
    tblUsers.reloadData()
    tblUsers.dragDelegate = self
    if arrUserList.count > 0 {
    DispatchQueue.main.asyncAfter(deadline: .now()+0.2) {
    self.tblUsers.selectRow(at: IndexPath(row: 0, section: 0), animated: false, scrollPosition: .top)
    self.setDataInDetailVC(model: self.arrUserList[0])
    }
    }
    }
    }
    
  9. In order to split the iPad screen for viewing two apps side by side, follow this code.

    //MARK:- UITableViewDragDelegate Delegate
    
    extension ChatListViewController: UITableViewDragDelegate {
    
    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    
    let selectedMessage = arrUserList[indexPath.row]
    
    let userActivity = selectedMessage.openDetailUserActivity
    
    let itemProvider = NSItemProvider(object: UIImage(named: selectedMessage.displayImage)!)
    
    itemProvider.registerObject(userActivity, visibility: .all)
    
    let dragItem = UIDragItem(itemProvider: itemProvider)
    
    dragItem.localObject = selectedMessage
    
    return [dragItem]
    
    }
    
    }
    
    //MARK:- UITableViewDelegate Delegate and UITableViewDataSource
    
    extension ChatListViewController : UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    return arrUserList.count
    
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    if let cell : ChatUsersTableViewCell = tableView.dequeueReusableCell(withIdentifier: "ChatUsersTableViewCell") as? ChatUsersTableViewCell {
    
    cell.cellConfig(user: arrUserList[indexPath.row])
    
    return cell
    
    }else {
    
    return UITableViewCell()
    
    }
    
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
    self.setDataInDetailVC(model: arrUserList[indexPath.row])
    
    }
    
    fileprivate func setDataInDetailVC(model:UsersModel) {
    
    if let vc : MainSplitViewController = self.navigationController?.parent as? MainSplitViewController {
    
    for childern in vc.children {
    
    if let navVC = childern as? UINavigationController,let childVC = navVC.viewControllers.first as? ChatViewController {
    
    childVC.userModel = model
    
    childVC.refreshData()
    
    }
    
    }
    
    }
    
    }
    
    }
    
  10. Add ChatViewController class to show Chat Detail.

    Get a UIViewController in main.storyboard and assign ChatViewController class to it.

    To show the detail of chat we will fetch dummy messages from MessageModel and display in the list.

    class ChatViewController: UIViewController {
        
        //MARK:- Variables
        var userModel : UsersModel!
        var isValidFromOtherWindow : Bool = false
        private var arrMessage : [Messages] = []
        private var currentUser : UsersModel = UsersModel(senderID: String(0), displayName: generateRandomName(), profession: generateProfessionName(), displayImage: "22")
        private var activeTextField : UITextView? = nil
        
        //MARK:- IBOutlets
        @IBOutlet weak var lblShadowMessage: UILabel!
        @IBOutlet weak var txtMessage: UITextView!
        @IBOutlet weak var tblMessages: UITableView!
        @IBOutlet weak var btnSend: UIButton!
        @IBOutlet weak var scrollView: UIScrollView!
        @IBOutlet weak var containerView: UIView!
        
        //MARK:- UIView Life Cycle
        override func viewDidLoad() {
            super.viewDidLoad()
            setUI()
            if (isValidFromOtherWindow) {
                self.navigationItem.hidesBackButton = true
                self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(dismissViewController))
                
            }
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(self.insertMessages(_:)),
                name: .messageAddedNotifiation,
                object: nil)
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            if (isValidFromOtherWindow) {
                self.refreshData()
            }
            self.registerKeyboardNotifications()
            guard let user = userModel else {
                return
            }
            if #available(iOS 13.0, *) {
                view.window?.windowScene?.userActivity = user.openDetailUserActivity
            }
            
        }
        
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            self.deRegisterKeyboardNotifications()
            if #available(iOS 13.0, *) {
                view.window?.windowScene?.userActivity = nil
            }
        }
        
        //MARK:- Set UI and Chat Configration
        func refreshData() {
            arrMessage = generateRandomMessages(currentUser: currentUser, otherUser: userModel)
            guard self.tblMessages != nil else {
                return
            }
            self.tblMessages.reloadData()
            self.tblMessages.scrollToBottom()
        }
        
        fileprivate func setUI() {
            
            txtMessage.layer.cornerRadius = 4
            if (!isValidFromOtherWindow) {
                txtMessage.addDoneButtonOnKeyboard()
            }
            
            tblMessages.register(UINib(nibName: "IncommingChatMessageTableViewCell", bundle: nil), forCellReuseIdentifier: "IncommingChatMessageTableViewCell")
            tblMessages.register(UINib(nibName: "OutgoingChatMessageTableViewCell", bundle: nil), forCellReuseIdentifier: "OutgoingChatMessageTableViewCell")
            tblMessages.estimatedRowHeight = 70
            tblMessages.rowHeight = UITableView.automaticDimension
            tblMessages.reloadData()
            
        }
    
  11. This method is responsible for creating window scenes and creating multiple windows. 

    @objc fileprivate func dismissViewController() {
            if #available(iOS 13.0, *) {
                var currentSession : UISceneSession? = nil
                for session in UIApplication.shared.openSessions {
                    if let scene = session.scene,let currentScene = view.window?.windowScene,scene == currentScene {
                        currentSession = session
                    }
                }
                guard let session = currentSession else {
                    return
                }
                UIApplication.shared.requestSceneSessionDestruction(session, options: nil) { (error) in
                    
                }
            }
            
        }
        
        
        fileprivate func setUpdateLayout() {
            self.view.updateConstraints()
            self.view.layoutIfNeeded()
            self.view.setNeedsLayout()
        }
        
        //MARK:- IBAction
        @IBAction fileprivate func btnSendAction(_ sender: UIButton) {
            NotificationCenter.default.post(name: .messageAddedNotifiation, object: self.userModel, userInfo: ["data":[txtMessage.text ?? ""]])
            txtMessage.text = ""
            lblShadowMessage.text = ""
            txtMessage.resignFirstResponder()
        }
        
    }
     
    //MARK:- UITableViewDelegate and UITableViewDataSource
    extension ChatViewController : UITableViewDelegate,UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return arrMessage.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let message = arrMessage[indexPath.row]
            if message.sender.senderId == self.currentUser.senderId {
                if let cell = tableView.dequeueReusableCell(withIdentifier: "OutgoingChatMessageTableViewCell") as? OutgoingChatMessageTableViewCell {
                    cell.cellConfig(model: message)
                    return cell
                }
            }else {
                if let cell = tableView.dequeueReusableCell(withIdentifier: "IncommingChatMessageTableViewCell") as? IncommingChatMessageTableViewCell {
                    cell.cellConfig(model: message)
                    return cell
                }
            }
            
            return UITableViewCell()
        }
        
        
    }
    
    //MARK:- UITextViewDelegate
    extension ChatViewController : UITextViewDelegate {
        func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
            activeTextField = textView
            return true
        }
        func textViewDidBeginEditing(_ textView: UITextView) {
            
        }
        
        func textViewDidEndEditing(_ textView: UITextView) {
            activeTextField = nil
        }
        
        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            let currentText = textView.text ?? ""
            guard let stringRange = Range(range, in: currentText) else { return false }
            let updatedText = currentText.replacingCharacters(in: stringRange, with: text)
            let previousHeight = lblShadowMessage.frame.height
            lblShadowMessage.text = updatedText
            self.tblMessages.layoutIfNeeded()
            self.setUpdateLayout()
            let newHeight = lblShadowMessage.frame.height - previousHeight
            tblMessages.contentOffset = CGPoint(x: 0, y: tblMessages.contentOffset.y + newHeight)
            self.tblMessages.layoutIfNeeded()
            return true
        }
    }
    
    //MARK:- InputBarAccessoryViewDelegate
    extension ChatViewController  {
        
        @objc fileprivate func insertMessages(_ notification:NSNotification) {
            guard let user = notification.object as? UsersModel else {
                return
            }
            guard let userModel = self.userModel else {
                return
            }
            if userModel.senderId != user.senderId {
                return
            }
            guard let dictData = notification.userInfo as? [String:Any],let data = dictData["data"] as? [String] else {
                return
            }
            if arrMessage.count == 0 {
                return
            }
            
            for component in data {
                if component.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 {
                    continue;
                }else{
                    let message = Messages(sender: currentUser, messageId: String(Int(arrMessage[arrMessage.count-1].messageId) ?? 0 + 1), sentDate: Date(), message: component.trimmingCharacters(in: .whitespacesAndNewlines))
                    arrMessage.append(message)
                }
                
            }
            self.tblMessages.reloadData()
            self.tblMessages.setNeedsDisplay()
            self.tblMessages.scrollToBottom()
        }
    }
    
    //MARK: - Keyboard Notification observer Methods
    extension ChatViewController {
        
        fileprivate func registerKeyboardNotifications() {
                
            NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    
            NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
            
    
        }
        
        fileprivate func deRegisterKeyboardNotifications() {
                   
            NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
            NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardDidHideNotification, object: nil)
        }
        
        @objc fileprivate func keyboardWillShow(notification: NSNotification) {
            
            if let activeTextField = activeTextField { // this method will get called even if a system generated alert with keyboard appears over the current VC.
                
                let info: NSDictionary = notification.userInfo! as NSDictionary
                let value: NSValue = info.value(forKey: UIResponder.keyboardFrameEndUserInfoKey) as! NSValue
                let keyboardSize: CGSize = value.cgRectValue.size
                
                let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardSize.height, right: 0.0)
                scrollView.contentInset = contentInsets
                scrollView.scrollIndicatorInsets = contentInsets
                
                var aRect: CGRect = self.view.frame
                aRect.size.height -= keyboardSize.height
                let activeTextFieldRect: CGRect? = activeTextField.convert(activeTextField.frame, to: self.containerView)//activeTextField.frame
                
                let activeTextFieldOrigin: CGPoint? = activeTextFieldRect?.origin
                if (!aRect.contains(activeTextFieldOrigin!)) {
                    scrollView.scrollRectToVisible(activeTextFieldRect!, animated:true)
                }
            }
        }
        
        
        @objc fileprivate func keyboardWillHide(notification: NSNotification) {
            
            let contentInsets: UIEdgeInsets = .zero
            scrollView.contentInset = contentInsets
            scrollView.scrollIndicatorInsets = contentInsets
        }
    }
    

    Concluding Remarks

    Apple might have introduced this feature quite late but you should not wait for anything now. If your app is the type that requires integrating multiple windows feature, then go for it.  We hope that you found this iPhone tutorial useful in clearing your concept about this new feature. 

    In case, if you have any suggestions or queries in this tutorial or any questions related to iPhone app development, we are all ears. You may also feel free to tell us what we have missed in this or if you feel something is unclear.

    We are a leading iPhone app development company and have hands-on experience in developing iOS apps with top features and functionalities. If you wish to develop a performance-oriented app with advanced features like this or want to hire iPhone developers, contact us. You may schedule a 30-min free consultation with our iOS developers and expert. All you need to do is fill the contact us form in the footer.

    You may also like:

    This page was last edited on November 11th, 2020, at 7:40.

Author Bio

Hitesh Trivedi

Hitesh Trivedi

Designation: iOS Team Lead

Hitesh Trivedi is an iOS Team Lead at Space-O Technologies. He has over 9 years of experience in iOS app development. He has guided to develop over 100 iPhone apps with unique features and functionalities. He has special expertise in Swift and Objective-C.

 
 

Have an App Idea?

Get your free consultation now