iMessage Extension: Callback when switching view (compact/enlarge) - ios

In my iMessage Extension, I am displaying stickers to the user, which are only displayed correctly, when the MSStickerView has the correct size. I resize it using sizeToFit(). However, switching between views changes its size to the maximum possible. Is there a function called whenever the switch button is pressed that I can add my code into and can you point me to the function that actually does the maximum resizing when switching (the one I do not want)?

You need to override func didTransition(to: MSMessagesAppPresentationStyle)
It will be called when the presentation has finished changed.
override func didTransition(to: MSMessagesAppPresentationStyle) {
super.didTransition(to: to)
switch to {
case .compact:
// Do your compact presentation
break
case .expanded:
// Do your expanded presentation
break
}
API reference : https://developer.apple.com/documentation/messages/msmessagesappviewcontroller/1649192-didtransition

Related

How to detect when user toggles sidebar hidden in split view controller to preserve secondary only display mode?

I’m using UISplitViewController to create a sidebar for my app - its style is .doubleColumn and I leave the preferredDisplayMode set to its default automatic behavior. It appears like the Photos app where in landscape both the primary and secondary columns are visible and in portrait only the secondary is visible (with a back button to reveal the primary overtop).
When in landscape, if the user taps the toggle sidebar button to hide the primary column (or toggles it via keyboard shortcut), rotates to portrait, and then rotates back to landscape, the sidebar undesirably becomes unhidden. This is unlike the Photos app where once the user hides the sidebar, it stays hidden until they unhide it (even across app launches). I want to achieve that same behavior.
To do this, I figured I could use the delegate function splitViewController(_willChangeTo:) and check if the new display mode is .secondaryOnly and the old display mode is .oneBesideSecondary then I’d store a bool in UserDefaults indicating the user hid the sidebar and I'd set the preferredDisplayMode to .secondaryOnly to preserve its hidden state between rotation. On the next app launch I’d check if that’s true and set the preferredDisplayMode to .secondaryOnly. (And similarly reset the flag and preferred display mode to .automatic when going from secondary only to one beside secondary.) The problem is that delegate function gets called with those same states when you rotate the device, which would cause me to set the flag and preferred display mode inappropriately. I need to only do that when the user manually toggled the sidebar, not when the system hid it due to a change in available space for example.
How can this be achieved?
I brought this question to a lab at WWDC where I received a good solution! In order to preserve the sidebar's hidden state between rotation, you can implement viewWillTransition(to:with:) to set a flag such as systemIsChangingViewSize to true, call super, then set it to false. In the delegate function .splitViewController(_:willChangeTo:) check this flag. It will be true when the system initiated a size change and false when the user initiated it by toggling the sidebar. Execute the logic noted in the question only if it's false in order to preserve the user's desired sidebar visibility.
private var systemIsChangingViewSize = false
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
systemIsChangingViewSize = true
super.viewWillTransition(to: size, with: coordinator)
systemIsChangingViewSize = false
}
func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
navigationDelegate?.navigationSplitViewController(self, willChangeDisplayMode: displayMode)
if !systemIsChangingViewSize {
if displayMode == .secondaryOnly && svc.displayMode == .oneBesideSecondary {
DispatchQueue.main.async {
svc.preferredDisplayMode = .secondaryOnly
}
} else if displayMode == .oneBesideSecondary && svc.displayMode == .secondaryOnly {
DispatchQueue.main.async {
svc.preferredDisplayMode = .automatic
}
}
}
}

Supported Interface Orientations in info.plist seem to block calls to shouldAutorotate

I’m designing an iPad/iPhone application in which the interface is designed to look as good in landscape as in portrait, and I’d like the interface to appear at launch as gracefully in one orientation as the other. At the same time, I need the ability to enable/disable autorotations at various times. I think I know how to do both these things, but they don’t seem to play nicely together.
I can set permitted device orientations in the Xcode project, and this seems to create a couple of array items in the project info.plist: "Supported interface orientations (iPad)" and "Supported interface orientations (iPhone)" which list the allowed orientations for each type of device. The presence of these items makes transitions from the launch screen as smooth as silk in all orientations.
I can enable/disable autorotations by overriding shouldAutorotate in my root view controller (there is only one controller at the moment). The problem is that the presence of Supported Interface items in info.plist seem to suppress all calls to shouldAutorotate, thus rendering control of autorotation inoperative.
class RootController: UIViewController
{
var allowAutorotation = true // My autorotation switch.
{ didSet { guard allowAutorotation else { return }
UIViewController.attemptRotationToDeviceOrientation()
/* Do other stuff. */ } }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask
{ NSLog ("RootController: supportedInterfaceOrientations")
return .allButUpsideDown }
override var shouldAutorotate: Bool
{ NSLog ("RootController: shouldAutorotate")
return allowAutorotation }
override func viewDidLoad()
{ allowAutorotation = true
super.viewDidLoad() }
}
I’m using viewDidLoad to call attemptRotationToDeviceOrientation() at the earliest possible moment. If I don’t do this, the app screen appears in Portrait even when the device is in Landscape. (It appears correctly for Portrait, but Portrait isn’t correct.) I’ve tried making a similar call from other places, including didFinishLaunchingWithOptions in the app delegate, which I think may be the most logical place for it, but it doesn’t seem to make a difference.
If I remove the Supported Orientation items from info.plist, and define allowed orientations in code as shown, I can see from my NSLog telltales that calls to shouldAutorotate do occur at appropriate moments, but the launch then looks a trifle awkward in landscape. In the launch screen, the status bar is oriented wrong: along one of the vertical edges rather than at top, and when the transition to the app screen comes, it typically fades in canted about 10-20° off of horizontal. It instantly snaps to horizontal (it’s so quick, in fact, that it’s sometimes difficult to see), and the status bar is then correctly at the top, and from that moment on everything looks good, but the effect still seems a little unprofessional.
Since it only occurs momentarily, and only once at launch, I suppose I could live with it (I can’t live without an autorotation control) but I was hoping someone could suggest a way to get shouldAutorotate calls to work even with orientations defined in info.plist. Or perhaps some other strategy?
I think I have workarounds for these problems. To eliminate the anomalous rotation animation as the Launch Screen fades out, I use CATransactions to disable implicit animations until the app becomes active for the first time:
class RootController: UIViewController {
private var appIsLaunching = true // Set false when app first becomes active. Never set true again.
func appDidBecomeActive()
{ if appIsLaunching { appIsLaunching = false; CATransaction.setDisableActions (false) } }
override func viewWillTransition (to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{ if appIsLaunching { CATransaction.setDisableActions (true) }
super.viewWillTransition (to: size, with: coordinator) }
}
And in the appDelegate:
func applicationDidBecomeActive ( _ application: UIApplication )
{ let root = window!.rootViewController as! RootController; root.appDidBecomeActive() }
I believe this works because a default CATransaction is always in effect, even without an explicit transaction. The cross-dissolve from Launch Screen to first view seems perfect.
There remains the status bar appearing in the wrong orientation in the Launch Screen. I’ve concluded it’s best to just turn it off, either in the project settings or by setting Status bar is initially hidden in info.plist. Not ideal, but acceptable.

How to retain previous selection in collectionview after reload in Apple TV

Hi in my Apple TV application i have one left collectionview right collectionview.Like splitview.When ever i focus cell on left data will refresh on right and when i select any cell in right collection view i am refreshing left and right collectionviews with new data (Like next level).And when on click on menu i will refresh both collectionviews with old data (Like coming to previous level). I want to highlight cell in left collectionview with red colour but i am reloading left collectionview while going forward or coming backward so always first cell is highlighting with Red colour. Can anyone suggest how to maintain previous selection in left collection-views because i am using only one collectionview for left menu and just reloading data.
The easiest way to retain focus in a UITableView or UICollectionView is to use UICollectionView.remembersLastFocusedIndexPath = true. This will automatically restore focus to the last focused item in a collection/table view and also automatically focus on the first item if there was no previously focused item or if the collection view data is reloaded.
If you need more control, the next level is to set UICollectionView.remembersLastFocusedIndexPath = false and use UICollectionViewDelegate.indexPathForPreferredFocusedView from your UIViewController instead. This method is only called whenever focus changes to a collection view programmatically though (but not if focus changes to a collection view as a result of TV remote interaction).
Now to ensure that indexPathForPreferredFocusedView is called when you switch between the left and right collection views using a TV remote, you will need to intercept shouldUpdateFocusInContext to override focus switches between the left and right collection view programmatically:
override func shouldUpdateFocusInContext( ... ) -> Bool {
if let nextView: UIView = context.nextFocusedView, let previousView: UIView = context.previouslyFocusedView{
if (nextView.isDescendant(of:leftCollectionView) && previousView.isDescendant(of:rightCollectionView)){
setFocusTo(leftCollectionView) // will invoke delegate indexPath method
return false // prevent system default focus change in favor of programmatic change
}
else if (nextView.isDescendant(of:rightCollectionView && previousView.isDescendant(of:leftCollectionView){
setFocusTo(rightCollectionView) // will invoke delegate indexPath method
return false
}
}
return true
}
internal var focusedView: UIView?
internal func setFocusTo(_ view:UIView){
focusedView = view
setNeedsFocusUpdate()
}
override var preferredFocusEnvironments -> [UIFocusEnvironment]{
return focusedView != nil ? [focusedView!] : super.preferredFocusEnvironments
}
func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
...
}
Alternatively, instead of using setFocusTo( collectionView ) + indexPathForPreferredFocusedView, you can just use setFocusTo( collectionViewCell ). Overriding indexPathForPreferredFocusedView is more robust though since it catches all cases where focus shifts for reasons other than user interaction (ex: system focus update due to an alert displaying + dismissing)

Detect when UISplitViewController changes display mode

I'm trying to use a UISplitViewController where the secondary controller should expose a "close" function (via a button or button bar item) whenever the UISplitViewController is in side-by-side mode, but should hide the function at other times. I tried putting this in the secondary view controller:
override func viewWillAppear(_ animated: Bool) {
if splitViewController!.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
This correctly sets the visibility of the "close" function when the secondary view is first displayed, but if the UISplitViewController switches between expanded and collapsed (say, by rotating an iPhone 6s Plus), then this function is not called again (which makes sense, as the secondary controller remains visible). Consequently, the "close" function remains in its initial state--hidden or shown--even as the UISplitViewController changes mode.
How can I get the "close" function to hide or show in reaction to changes in the mode of the UISplitViewController?
There is the UIViewControllerShowDetailTargetDidChangeNotification notification for that:
// Sometimes view controllers that are using showViewController:sender and
// showDetailViewController:sender: will need to know when the split view
// controller environment above it has changed. This notification will be
// posted when that happens (for example, when a split view controller is
// collapsing or expanding). The NSNotification's object will be the view
// controller that caused the change.
UIKIT_EXTERN NSNotificationName const UIViewControllerShowDetailTargetDidChangeNotification NS_AVAILABLE_IOS(8_0);
Use as follows
- (void)viewDidLoad{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(showDetailTargetDidChange:) name:UIViewControllerShowDetailTargetDidChangeNotification object:self.splitViewController];
}
- (void)showDetailTargetDidChange:(NSNotification *)notification{
// changed from collapsed to expanded or vice versa
}
This Apple sample demonstrates how the table cell accessory changes from a disclosure indicator in portrait (denoting that a push will happen) to it being removed when changing to landscape split view:
https://developer.apple.com/library/archive/samplecode/AdaptivePhotos/Introduction/Intro.html
Note on iOS 13 beta, use addObserver with object nil because there currently is a bug they send the notification using the wrong object. They use their new UISplitViewControllerPanelImpl from the internal class cluster instead of the UISplitViewController object.
http://www.openradar.appspot.com/radar?id=4956722791710720
For future reference:
What about using the UISplitViewControllerDelegate??
It has a method called
splitViewController:willChangeToDisplayMode:
that should do exactly what you where looking for.
Documentation here
Okay, I found a simple solution. I was making a novice mistake. The trick is to override viewWillLayoutSubviews() instead of viewWillAppear(animated:). Then everything works as I want. It seems that viewWillLayoutSubviews() is called (sometimes more than once) every time the containing UISplitViewController changes its display mode, and that's exactly what I need to respond to. The only gotcha is that splitViewController might be nil on some of those calls, so it needs to be implemented like this:
override func viewWillAppear(_ animated: Bool) {
if let svc = splitViewController {
if svc.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
}
As part of my stumbling around to find a solution, I tried overriding traitCollectionDidChange(previousTraitCollection:). (I tried this because I wanted to react to device rotations.) At first I thought I was onto something, because this function also get called whenever the device rotates. Interestingly (and, frustratingly) I found that my view's splitViewController property was nil when this function is called. It seems odd that this should be so, since neither viewDidDisappear(animated:) nor viewWillAppear(animated:) is called when the UISplitViewController reconfigures itself. But why it should be nil is, I suppose, a question for another day.

How can I add a Number and Decimal keyboard into my Custom Keyboard on iOS8?

I created a custom keyboard in Swift but it was rejected from the App Store because: "keyboard extension does not include Number and Decimal types".
How can I add these two keyboards easily?
I tried to rebuild the original view but it doesn't work properly. I'm sure there is a solution to create 2 or 3 different views and switch between them.
How can I switch between keyboard types when the keyboard type changes?
You can detect changes to the keyboard type in textDidChange. You need to get the UITextDocumentProxy then detect the proxy's keyboardType, and then you can do whatever layout changes needed.
override func textDidChange(_ textInput: UITextInput?) {
// Called when the document context is changed - theme or keyboard type changes
let proxy = self.textDocumentProxy as UITextDocumentProxy
if proxy.keyboardType == UIKeyboardType.numberPad
|| proxy.keyboardType == UIKeyboardType.decimalPad {
//add code here to display number/decimal input keyboard
}
}

Resources