Xcode XIB: how to implement a ScrollView or PageControl to swipe sub-views? - xcode

I'm building a typical Xcode 6 iOS app.
My goal is:
A screen that has an sub-area that can be swiped to change the content.
For example, the home screen has a logo image, a middle area that I want to be swipeable, and a bottom button.
When the user swipes (or taps) the middle area, the area shows the next (or previous) information, which is a typical UIImage and UILabel caption.
The rest of the screen stays the same, i.e. there is no navigation change.
The code is here. It use the recommendations from the StackOverflow post here.
My question: how can I implement the code below better, while still using an XIB?
My current implementation does work, and uses this approach...
A typical Swift Demo.swift file that is a UIViewController that has:
the page index, min, and max
outlets for the PageControl, UIImageView, and UILabel
actions for the page control change, and the image swipe or tap
A typical Demo.xib file that has:
a typical UIViewController for the entire screen
a UIImageView and UILabel for the changeable image and caption text
a PageControl to indicate what tutorial page the user is viewing
I am seeking better ways to accomplish this; I've read many of Xcode tutorials and so far none seem definitive for Xcode 6, XIBs, and Swift.
Here are some implementations that I've researched that seem promising...
Is there a way to implement a subview area in the XIB?
For example, can Xocde show the XIB with a rectangular area that is intended for the changeable content?
Is there an idiomatic way to write the code for changeable content?
For example, by using a ScrollView, perhaps that contains a UIPageViewController?
Is there a way to make a PageControl XIB object large enough to cover the entire UIImageView and UILabel, so I can skip making the UIImageView respond to gestures.
In my Xcode, the PageControl seems to have an uneditable height that is always 37.
The bounty will be for expert advice.

To make a UIPageViewController swipe-able you should implement the UIPageViewControllerDataSource protocol and provide a view controller for the pageViewController(pageViewController:viewControllerBeforeViewController) -> UIViewController? and the ...viewControllerAfterViewController) methods.
Provide a custom view controller for each page that presents an image and label and takes them as properties so you can provide them from the PageViewController.
My trick it to create a method that instantiates a new view controller in these methods:
// MARK:- UIPageViewControllerDataSource
extension MyPageViewController: UIPageViewControllerDataSource {
func viewControllerWithIndex(var index: Int) -> UIViewController! {
let viewController = storyboard?.instantiateViewControllerWithIdentifier("MyViewController") as! MyViewController // This VC has to be in the storyboard, otherwise just use MyVC()
// Adjust the index to be cyclical, not required
if let count = data?.endIndex {
if count == 1 && index != 0 { return nil }
if index < 0 { index += count }
index %= count
}
viewController.view.tag = index
viewController.record = data?[index]
return viewController
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let index = viewController.view?.tag ?? 0
return viewControllerWithIndex(index + 1)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let index = viewController.view?.tag ?? 0
return viewControllerWithIndex(index - 1)
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return countAndSetupPageControl()
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return viewController?.view.tag ?? 0
}
}
Now for the "sub-area" you will need to implement a ChildViewController. If you're using storyboards you can just drag a Container View and put PageViewController in the embedded view controller, otherwise you need to add the PageViewController.view as a subview and set the frame to the middle.
You can find more info in the apple documentation but basically you MUST call these methods:
addChildViewController(pageViewController)
view.addSubView(pageViewController.view)
pageViewController.view.frame = ... // This is your "sub-area"
pageViewController.didMoveToParentViewController(self)

If you add a height constraint to PageControl you can set it's height to whatever you want.
I don't see a problem with your current implementation. Changing it to use a PageViewController would be quite more work.
If I were you I would add an animation in pageUpdate function so the image would fade in or slide in...
It would only make sense to use a PageViewController if you want to be able to scroll to the next page (as in content moving in the same time your finger is moving onscreen). And you can use a PageViewController or a CollectionView with paging enabled.

Related

How to align a toolbar (or its items) with the leading edge of a split view controller's child?

In iOS, a toolbar can be added to any view. In macOS however, it seems only possible to add a toolbar to a window.
I'm working on an app with a split view controller with a toolbar but the toolbar's items only have a meaning with respect to the right view controller's context.
E.g. let's say I have a text editor of some sort, where the left pane shows all documents (like in the Notes app) and the right pane shows the actual text which can be edited. The formatting buttons only affect the text in the right pane. Thus, it seems very intuitive to place the toolbar within that right pane instead of stretching it over the full width of the window.
Is there some way to achieve this?
(Or is there a good UX reason why this would be a bad practice?)
I've noticed how Apple solved this problem in terms of UX in their Notes app: They still use a full-width toolbar but align the button items that are only related to the right pane with the leading edge of that pane.
So in case, there is no way to place a toolbar in a view controller, how can I align the toolbar items with the leading edge of the right view controller as seen in the screenshot above?
Edit:
According to TimTwoToes' answer and the posts linked by Willeke in the comments, it seems to be possible to use Auto Layout for constraining a toolbar item with the split view's child view. This solution would work if there was a fixed toolbar layout. However, Apple encourages (for a good reason) to let users customize your app's toolbar.
Thus, I cannot add constraints to a fixed item in the toolbar. Instead, a viable solution seems to be to use a leading flexible space and adjust its size accordingly.
Initial Notes
It turns out this is tricky because there are many things that need to be considered:
Auto Layout doesn't seem to work properly with toolbar items. (I've read a few posts mentioning that Apple has classified this as a bug.)
Normally, the user can customize your app's toolbar (add and remove items). We should not deprive the user of that option.
Thus, simply constraining a particular toolbar item with the split view or a layout guide is not an option (because the item might be at a different position than expected or not there at all).
After hours of "hacking", I've finally found a reliable way to achieve the desired behavior that doesn't use any internal / undocumented methods. Here's how it looks:
How To
Instead of a standard NSToolbarFlexibleSpaceItem create an NSToolbarItem with a custom view. This will serve as your flexible, resizing space. You can do that in code or in Interface Builder:
Create outlets/properties for your toolbar and your flexible space (inside the respective NSWindowController):
#IBOutlet weak var toolbar: NSToolbar!
#IBOutlet weak var tabSpace: NSToolbarItem!
Create a method inside the same window controller that adjusts the space width:
private func adjustTabSpaceWidth() {
for item in toolbar.items {
if item == tabSpace {
guard
let origin = item.view?.frame.origin,
let originInWindowCoordinates = item.view?.convert(origin, to: nil),
let leftPane = splitViewController?.splitViewItems.first?.viewController.view
else {
return
}
let leftPaneWidth = leftPane.frame.size.width
let tabWidth = max(leftPaneWidth - originInWindowCoordinates.x, MainWindowController.minTabSpaceWidth)
item.set(width: tabWidth)
}
}
}
Define the set(width:) method in an extension on NSToolbarItem as follows:
private extension NSToolbarItem {
func set(width: CGFloat) {
minSize = .init(width: width, height: minSize.height)
maxSize = .init(width: width, height: maxSize.height)
}
}
Make your window controller conform to NSSplitViewDelegate and assign it to your split view's delegate property.1 Implement the following NSSplitViewDelegate protocol method in your window controller:
override func splitViewDidResizeSubviews(_ notification: Notification) {
adjustTabSpaceWidth()
}
This will yield the desired resizing behavior. (The user will still be able to remove the space completely or reposition it, but he can always add it back to the front.)
1 Note:
If you're using an NSSplitViewController, the system automatically assigns that controller to its split view's delegate property and you cannot change that. As a consequence, you need to subclass NSSplitViewController, override its splitViewDidResizeSubviews() method and notify the window controller from there. Your can achieve that with the following code:
protocol SplitViewControllerDelegate: class {
func splitViewControllerDidResize(_ splitViewController: SplitViewController)
}
class SplitViewController: NSSplitViewController {
weak var delegate: SplitViewControllerDelegate?
override func splitViewDidResizeSubviews(_ notification: Notification) {
delegate?.splitViewControllerDidResize(self)
}
}
Don't forget to assign your window controller as the split view controller's delegate:
override func windowDidLoad() {
super.windowDidLoad()
splitViewController?.delegate = self
}
and to implement the respective delegate method:
extension MainWindowController: SplitViewControllerDelegate {
func splitViewControllerDidResize(_ splitViewController: SplitViewController) {
adjustTabSpaceWidth()
}
}
There is no native way to achieve a "local" toolbar. You would have to create the control yourself, but I believe it would be simpel to make.
Aligning the toolbar items using autolayout is described here. Align with custom toolbar item described by Mischa.
The macOS way is to use the Toolbar solution and make them context sensitive. In this instance the text attribute buttons would enable when the right pane has the focus and disable when it looses the focus.

Simple displaying of second window: OS X & Swift

I'm trying to find how to bring up a second view/window after pushing a button on my primary window. I have read about segues and I can get the first window to display the second but the second is not connected to a view controller so I can't add any code to any controls on the second view. Try as I might I cannot create a SecondViewController.swift file and connect it to a window controller or a view controller. The tutorials I have found all deal with iOS and I want OS X which means there are just enough differences to keep me from figuring this out.
Can anyone show me how to do this?
Ta,
A.
First make new file like:
After that, put these codes in your classes and that should do it.
class SecondWindowController: NSWindowController {
convenience init() {
self.init(windowNibName: "SecondWindowController")
}
}
class ViewController: NSViewController {
private var secondWindowController: SecondWindowController?
#IBAction func showSecondWindow(sender: AnyObject) {
if secondWindowController == nil {
secondWindowController = SecondWindowController()
}
secondWindowController?.showWindow(self)
}
}

Use NSToolBar Outlet xcode 6 and Storyboard?

I am trying to add an outlet into my viewcontroller for a toolbar item in my window controller. I have tried playing around with first responder and bindings but have not been able to find any solutions.
A similar question that was answered provided some insight but no one has mentioned anything about IBOutlets other than still asking how to add them in the comments. The answer has been accepted so i am assuming no one will add to it.
How to use NSToolBar in Xcode 6 and Storyboard?
Incase my question is unclear at all, i would like to be able to add this to my storyboard program
#IBOutlet weak var Mytoolbar: NSToolbarItem!
func enabletoolbar()
{
Mytoolbar.action = "FunctionIn.ViewController.swift"
Mytoolbar.enabled = true
}
I found a decent workaround by adding IBOutlets to my custom NSWindow class and using the storyboard to connect my views to the IBOutlets. Then, I accessed these views from my NSViewController class by getting them from the custom NSWindow.
Basically you need to set the action and other properties to the toolbaritem but not in the toolbar. So try the same.
i ended up doing this in my view controller which seems to work
override func viewDidLayout() {
var x = self.view.window?.toolbar?.items[1].label
println(x)
if(self.view.window?.toolbar?.items[0].label! != "Check")
{
toobarediting()
}
println("didlay")
}
func toobarediting() {
self.view.window?.toolbar?.insertItemWithItemIdentifier("Check", atIndex: 0)
}
func toolbarcheck(functiontoset: Selector) {
var y = self.view.window?.toolbar?.items[0] as NSToolbarItem
y.action = functiontoset
if(functiontoset != nil)
{
y.enabled = true
}
}
It seems to allow me to make the tool bar button clickable/unclickable when ever i require it to change it just seems so much more bulky and error prone than
myitem.enable = fale
myitem.action = nil
is this really the best way for a storyboard based application in osx?
While connectiong IBActions works by using either the First Responder or by adding an "Object" to the scene, then changing its class to the window's view controller class, this doesn't help with IBOutlets and delegates that you'd like to point to the view controller.
Here's a work-around for that:
Add the Toolbar to the View Controller, not to its Window. That way, you can make all the IBOutlet connections in the View Controller Scene easily. I've done that for years and found no issues with it, even when using Tabs.
You'll have to assign the window's toolbar in code, then. E.g. like this:
#interface ViewController ()
#property (weak) IBOutlet NSToolbar *toolbar; // connect this in your storyboard to the Toolbar that you moved to the View Controller Scene
#end
- (void)viewWillAppear {
[super viewWillAppear];
self.view.window.toolbar = self.toolbar;
}

Custom cell in Xcode 6 + Swift not displaying

I've searched a lot on the internet for a solution to this problem but I can't figure it out. I'm trying to create a custom cell in a table view.
I made a CustomCell.swift class to configure the labels I want in my custom cell, created it via storyboard (the first prototype cell in the tableview) and linked it with a identifier to the cellForRowAtIndexPath method
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cellIdentifier = "huisCell"
var cell: CustomCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? CustomCell
if cell == nil {
cell = CustomCell(style: UITableViewCellStyle.Value1, reuseIdentifier: cellIdentifier)
}
cell!.huisAdresLabel.text = "123"
cell!.huisDetailLabel.text = "456"
return cell
}
My CustomCell.swift code is like this:
class CustomCell: UITableViewCell {
#IBOutlet var huisAdresLabel: UILabel
#IBOutlet var huisDetailLabel: UILabel
}
It's very basic now, but I just want it to work because than I can expand the cell with more attributes and style it better.
Pictures via DropBox because I need 10 reputation to properly document my problem :)
https://www.dropbox.com/sh/5v9jb6cqp80knze/AAD5-yPR8-KoStQddkqKIbcUa
I hope someone can explain what I'm doing wrong.
Edit:
To clear up some things, before my try to make a custom cell, I got it working with the basic cells, with the one label on the left hand side. But when I tried to style the tableview and created a custom cell it won't work.
Also, when testing different solutions I came across the problem that de two labels in CustomCell.swift are nil. Even when I made a custom init and did like a
self.huisAdresLabel = UILabel()
it was still nil. in the code that I showed you it prints the following:
<UILabel: 0xb2aadc0; frame = (0 -21; 42 21); text = '123'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0xb2aa3a0>>
I resolved this issue by overriding the following function, setting the height of the cells manually:
override func tableView(tableView:UITableView!, heightForRowAtIndexPath indexPath:NSIndexPath)->CGFloat
{
return 44
}
Try using a xib file and adding your custom cell class to the
"Table View Cell" that you create in your xib file.
(Make sure you reconnect it to the outlets in your custom cell class ;)
This link may help.
http://www.weheartswift.com/swifting-around/
I had the same Problem, but after disabling the "Use Auto Layout" under the "File Inspector", it did work!! and the Custom Cells are displayed
Note: I made the Custom Cells in the Builder, not in Code. Using Xcode beta 3
Working w/ Custom table cells in Xcode 6 Beta-4's IB, I found they all rendered on top of each other and my non-Custom cell.
I fixed my problem by...
selecting the Custom cell in IB
selecting the Size inspector (Option-Command-5)
in the Table View Cell section
checking the Custom box
keeping the default-provided 44 Row Height
Quick workaround: disable Use Size Classes, but still don't know is it bug or feature :-) needs more investigation or man reading.

I want to go to a specific tab in a UITabBarController from a view controller

First of all, let me say I'm an absolute beginner, so please forgive me if this is a stupid questions to be asking. And let me just add that I've spent hours/days trying to figure out how to solve this problem - including extensive searches on stackoverflow (maybe the answer is somewhere and I just don't now exactly what to search for :)...
But, let's continue: I have a small (?) problem with an Xcode storyboard project. Basically my project looks like this:
Navigation Controller -> View Controller 0 -> Tab Bar Controller -< View Controller 1, View Controller 2, View Controller 3.
When the user pushes 'button #2' in the View Controller 0, I'd like him/her to jump directly to 'View Controller 2'.
Would that be possible at all, and if so what code should use and excatly where should I put it.
Hope someone out there will help a newbie out :)
Regards,
Ulrik
Yes it is possible. You may show any view controller from any other.
You should simply add a segue from button #2 to View Controller 2. (I assume you have all your controllers in single storyboard)
Update: the above solution will show you View Controller 2 itself without tab bar controller.
Hard to tell in details without seeing the actual code. For more details you may refer to these documents:
View Controller Basics (especially part "Storyboards Help You Design Your User Interface")
Presenting View Controllers from Other View Controllers
Using View Controllers in Your App
Probably you'll come up with more concrete question.
Update
If you want to preselect desired view controller inside tabbar controller you may use the following code sketch. Here you can programmatically initiate a segue and do the desired pre-initialization inside prepareForSegue:sender: method.
static NSString * const kShowTabSegueID = #"ShowTab";
#interface ViewController ()
- (IBAction)buttonOnePressed;
- (IBAction)buttonTwoPressed;
- (IBAction)buttonThreePressed;
#end
#implementation ViewController
- (IBAction)buttonOnePressed
{
[self performSegueWithIdentifier:kShowTabSegueID
sender:#0];
}
- (IBAction)buttonTwoPressed
{
[self performSegueWithIdentifier:kShowTabSegueID
sender:#1];
}
- (IBAction)buttonThreePressed
{
[self performSegueWithIdentifier:kShowTabSegueID
sender:#2];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue
sender:(id)sender
{
if ([segue.identifier isEqual:kShowTabSegueID]) {
NSNumber *indexToShow = sender;
UITabBarController *tabBar = segue.destinationViewController;
[tabBar setSelectedIndex:indexToShow.unsignedIntegerValue];
}
}
#end
If you are simply trying to programatically switch tabs, its as simple as:
[self.tabBarController setSelectedIndex:1];
If I am understanding your flow correctly, from ViewController0 you would present ViewController1(that has a UITabBarController). In the viewWillAppear: set the selectedIndex for the tab controller (code above) to index 1, which would be ViewController2.
EDIT
After looking at your project, add this code to your BrainBreaksViewController.m
- (void)viewWillAppear:(BOOL)animated
{
[self.tabBarController setSelectedIndex:1];
}
I added this, and it switches to the 2nd tab after pressing "Press this button to goto tab #1". Follow Max Ks answer if you would like to be able to have a button to open each specific tab.
So if I understand correctly you have a view controller outside the tabbar controller and you want to navigate to the second tab in the tabbar controller from that (outside)view controller, if that is the problem then, this is my solution and I hope it helps someone as I spent some time on this issue myself.
First connect the (outside)Viewcontroller 0 to the tabbar controller in the storyboard with modal segue and give it identifier - "showTabBar" (not as one of the tabs just a modal segue).
Then:
in Viewcontroller 0 declare:
var tabBarIndex: Int?
//function that will trigger the **MODAL** segue
private func loadTabBarController(atIndex: Int){
self.tabBarIndex = atIndex
self.performSegue(withIdentifier: "showTabBar", sender: self)
}
//in here you set the index of the destination tab and you are done
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showTabBar" {
let tabbarController = segue.destination as! UITabBarController
tabbarController.selectedIndex = self.tabBarIndex!
}
}
then you can navigate to ViewController 2 like that(don't forget it is 0 indexed):
self.loadTabBarController(atIndex: 1)
This is tested and working as of the day I am posting this answer using
Swift 3.0.2 / Xcode 8.2.1

Resources