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. Add 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:

 
 

Have an App Idea?

Get your free consultation now