ScrollView not reloading upon dismissing ViewController and calling viewDidLoad - swift

I have a scrollView that contains a dynamic amount of WeatherViewControllers each displaying the weather data of a different city the user has saved. The user can segue from the WeatherViewControllers to a CityListViewController. Where they can add and remove cities from their list which in turn should add and remove WeatherViewControllers from the scrollView upon dismissing the CityListViewController, this is where I am running into a problem.
Currently I am using a protocol to call viewDidLoad in the scrollViewController upon dismissing the CityListViewController which works as I follow the code with the debugger all of the code that should be getting called is and the variable which tracks how many viewControllers to create is accurate after the change, however the scrollView is not changing. If I add/remove a city from the list the scrollView still displays the same amount as before.
The func createAndAddWeatherScreen is called the accurate amount of times after the change and everything and if I close the app and reopen it the scrollView then displays the right amount of viewControllers. It seems like everything is working except the scrollView is not reloading upon dismissing the cityListController.
Side Note: Upon initially opening the app the scrollView loads properly with all the correct WeatherViewControllers in the UIScrollView and the correct cities in the list.
class ScrollViewController: UIViewController, ScrollReloadProtocol {
func reloadScrollView() {
self.viewDidLoad()
}
//#IBOutlet var totalScrollView: UIScrollView!
var pages = [ViewController]()
var x = 0
var weatherScreensArray = [SavedCityEntity]()
var weatherScreenStringArray = [String]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var horizString = "H:|[page1(==view)]"
let defaults = UserDefaults.standard
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isPagingEnabled = true
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
view = scrollView
//userDefaults used to keep track of which screen is which to put different cities on different viewControllers
defaults.set(0, forKey: "screenNumber")
//load cities to get number of cities saved
loadCities()
var views : [String: UIView] = ["view": view]
//create all weatherWeatherControllers
while x <= weatherScreensArray.count {
pages.append(createAndAddWeatherScreen(number: x))
weatherScreenStringArray.append("page\(x+1)")
views["\(weatherScreenStringArray[x])"] = pages[x].view
let addToHoriz = "[\(weatherScreenStringArray[x])(==view)]"
horizString.append(addToHoriz)
x+=1
}
horizString.append("|")
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[page1(==view)]|", options: [], metrics: nil, views: views)
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: horizString, options: [.alignAllTop, .alignAllBottom], metrics: nil, views: views)
NSLayoutConstraint.activate(verticalConstraints + horizontalConstraints)
}
//Function to create and add weatherViewController
func createAndAddWeatherScreen(number: Int) -> ViewController {
defaults.set(number, forKey: "screenNumber")
let story = UIStoryboard(name: "Main", bundle: nil)
let weatherScreen = story.instantiateViewController(identifier: "View Controller") as! ViewController
weatherScreen.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(weatherScreen.view)
addChild(weatherScreen)
weatherScreen.didMove(toParent: self)
return weatherScreen
}

There are several architectural issues with your code:
You should not be calling the viewDidLoad method manually, it's very easy to extract the necessary code for fetching and reloading into a method of its own which doesn't break the lifecycle of a viewController.
It looks like you're attempting to use the scrollView as a tableView which offers reloading as a free function, as well as far better memory management.
You could use .map() instead of using while to iterate and create new arrays manually, higher order functions are very useful.
Despite all that, you can't actually reload a scrollView in the way you can a tableView. It already contains the views you added there previously, and it looks to me like you're stacking extra views over and over again, you could debug if this is the case with the visual debugger.
Lastly, in order to update a view after updating constraints you need to call:
setNeedsLayout()
layoutIfNeeded()
to let the system know you want to update the subviews and their layout.

Related

Swift iPad - How to present a formSheet with custom size based on a table view content?

In my app i present a form sheet in this way for iPad devices:
myViewController = .formSheet
present(myViewController, animated: true)
This ViewController is very simple: there is just a table view displaying n elements.
My goal is make the formSheet size the same as the table view with its n elements.
I actually achieved that with this code:
class MyViewControlller: UIViewController {
#IBOutlet weak var myTable: UITableView!
private var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
myTable.delegate = self
myTable.dataSource = self
observer = myTable(\.contentSize, options: [.new], changeHandler: { [weak self] (table, _) in
guard let self = self else {
return
}
self.preferredContentSize = CGSize(width: table.contentSize.width, height: table.contentSize.height + 30.0)
self.view.layoutIfNeeded()
})
}
}
But the problem is that while presenting the formSheet (while it is sliding from bottom to the center of the screen) I can clearly see an animation where the bottom of the sheet is adjusting its size until it reaches the center of the screen.
What I'm trying to achieve is this: When the sheet starts sliding from the bottom to the center of the screen it already has the right dimensions.
Am I doing something wrong with the code?

Swift sub view hold old values (stacking new values on top of it)

[solved :)
by adding self.progressScrollView?.removeFromSuperview() ]
I am new to swift.
and having below issue :( please help me.
every time I come back to this main screen (using back button)
my scroll view's content are showing new values on top of old values
for example.
I have 4 items(a,b,c,d) in an array.
and main page show in random order
a
b
c
d
after that, i went to another page and came back to this screen.
it shows new values on top of old values
a -> b (this value is on top of 'a')
b -> d (this value is on top of 'b')
c -> c (this value is on top of 'c')
d -> a (this value is on top of 'd')
what I did?
I re-initialize my array in viewdidappear()
added below in gotopage()
dismiss(animated: true, completion: nil)
add removefromsuperview & view.addsubview in self.displayProgressBar()
(what #sandeep suggested.. but doesn't work)
my flow:
1.call displayprogressbar in viewdidload
self.displayProgressBar()
class ViewController: UIViewController ,ChartViewDelegate, UIScrollViewDelegate {
...
var progressScrollView: UIScrollView!
...
override func viewDidLoad() {
super.viewDidLoad()
self.getfirebasedata()
2.add progress bars and labels dynamically based on the list size.
func getfirebasedata(...){
...
self.displayProgressBar(...)
}
func displayProgressBar(...){
self.progressScrollView?.removeFromSuperview() //this is the fix
self.progressScrollView = UIScrollView()
self.progressScrollView.isScrollEnabled = true
self.progressScrollView.frame = CGRect(x: 0, y: 300,
width: self.view.frame.size.width, height: 200)
self.progressScrollView.contentSize = CGSize(width: self.view.frame.size.width,
height: self.view.frame.size.height)
let axisXstartPoint = ( self.view.frame.size.width - 200 ) / 2
let progressView = UIProgressView(progressViewStyle: .bar)
...
let label = UILabel()
label.frame = CGRect(x: axisXstartPoint, y: CGFloat(axisY-15), width: 200, height: 20)
label.textAlignment = .left
label.text = book.bookName
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapBookName)
progressScrollView.addSubview(label)
progressScrollView.addSubview(progressView)
self.view.addSubview(progressScrollView)
}
when I go to another page I use this method
#objc #IBAction func goToAddPage(sender: UITapGestureRecognizer) {
let label = sender.view as? UIButton
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let addViewController = storyBoard.instantiateViewController(withIdentifier: "add") as! AddViewController
addViewController.modalPresentationStyle = .fullScreen
self.navigationController?.pushViewController(addViewController, animated: true)
dismiss(animated: true, completion: nil)
}
4.this is my storyboard
Not sure from where you call displayProgressBar as its not clear from your question, what you can do is add these statement as either 1st statment in displayProgressBar or as a statement before calling displayProgressBar
Approach 1:
self.progressScrollView?.removeFromSuperview() //? because for the first time implicit optional will be nil
self.progressScrollView = UIScrollView()
In ViewDidLoad you have self.view.removeFromSuperview() which I have no idea why? Makes no sense to me why would you try to remove ViewController's view? May be you were trying to remove subviews I assume, if yes, thats not the proper way to remove subviews, you iterate over subViews and call removeFromSuperview on each subView, but I dont think you need that, clearly from your code you hold a reference to progressScrollView somewhere in your code, just remove it from subview and recreate the progressScrollView as
progressScrollView = UIScrollView()
And if you are not holding a reference to progressScrollView and instead you were creating it as only local variable in displayProgressBar method, hold a reference to this view, to do that declare a instance variable var progressScrollView: UIScrollView!
Approach 2:
And if you dont prefer creating progressScrollView() again and again, you can always iterate over its subviews and remove them from superview one by one
for view in progressScrollView.subviews {
view.removeFromSuperview()
}
Make sure you run self.view.addSubview(progressScrollView) only once (like in ViewDidLoad or somewhere) if you decide to go with approach 2.
In either case you need to hold a reference to progressScrollView

Cocoa, Swift 4 - How to non-programatically embed custom NSViewController when its storyboard is a different file than the app's Main.Storyboard?

I'm working on updating a MacOS program in Swift 4/Xcode 12. I have a custom view controller "HelloWorldViewController" which previously I was displaying in a window by itself. HelloWorldViewController uses the storyboard HelloWorldViewController.storyboard rather than Main.Storyboard. Here's the code for toggling the Window containing the view which works just fine.
func toggleHelloWorldWindow() {
if helloWorldViewCtrl == nil {
let myStoryboard = NSStoryboard(name: NSStoryboard.Name("HelloWorldView"), bundle: nil)
helloWorldWindowCtrl = myStoryboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("HelloWorldWindow")) as? NSWindowController
guard helloWorldWindowCtrl != nil else {return}
helloWorldViewCtrl = helloWorldWindowCtrl!.window!.contentViewController as? HelloWorldViewController
}
if let myWindowController = helloWorldWindowCtrl {
if myWindowController.window!.isVisible {
myWindowController.window!.orderOut(self)
} else {
myWindowController.showWindow(helloWorldWindowCtrl)
}
}
}
However I've decided that the best way to use the controller is to embed it in the main application rather than a separate window. It seemed that the way to do this was to use a NSContainerView which I was able to get working just fine if I had the storyboard for the custom view in the same storyboard file as the main app.
But for the life of me I've been unable to get it to point to an external storyboard. It always crashes in viewDidLoad of the custom view controller when I set values for items on the custom view page as they are all nil (ie, the view didn't render?)
class HelloWorldViewController: NSViewController {
#IBOutlet weak var helloWorld: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
helloWorld.stringValue = "Hello world..." (crashes)
}
}
After two days of fussing with it was able to make it work programmatically (which is just fine for my purposes and likely the solution I'll stick with) but I'm still wondering what was I missing in the process of trying to do it with exclusively storyboards? I tried to set up a segue but could not find any way to tell it the storyboard filename and just using the scene identifier resulted in the same crash.
(programatic version in main app's viewDidLoad() which works fine)
let myStoryboard = NSStoryboard(name: NSStoryboard.Name("HelloWorldView"), bundle: nil)
if let helloWorldWindowCtrl = myStoryboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("HelloWorldWindow")) as? NSWindowController,
let controller = helloWorldWindowCtrl.window!.contentViewController as? HelloWorldViewController
{
addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
helloWorldView.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: helloWorldView.leadingAnchor, constant: 0),
controller.view.trailingAnchor.constraint(equalTo: helloWorldView.trailingAnchor, constant: 0),
controller.view.topAnchor.constraint(equalTo: helloWorldView.topAnchor, constant: 0),
controller.view.bottomAnchor.constraint(equalTo: helloWorldView.bottomAnchor, constant: 0)
])
}

Swift, newbie question related to containerView. how to make Storyboard viewcontroller for child the same size as a container view

I've been trying to correctly implement a ContainerView in my practice app after watching and reading various tutorials. I initially tried creating the child by utilizing the storyboard and dragging the ContainerView onto the ViewController in question, and then utilizing the child ViewController that is then automatically created, but I need to have multiple child ViewControllers and I couldn't quite figure that out. I researched some programatic ways to do it and I successfully have it functioning the way I want. The only hiccup being that when I am viewing the child ViewControllers on my storyboard they are full size and do not correlate in size to my ContainerView. So I have to have a bit of trial and error in getting the objects I place in the child to fit in the ContainerView.
Can anyone give me some pointers on how I fix that? I've used the code below. The function runs when a button on the parent ViewController is touched. There are other associated child ViewControllers: child2, child3 that run depending on which button is pushed. I didn't include that extra code below for the sake of being concise.
private lazy var child1: PersonInfoChildView1Controller = {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
var viewController = storyboard.instantiateViewController(withIdentifier: "Child1VC") as! PersonInfoChildView1Controller
viewController.person = self.person
addChild(viewController)
return viewController
}()
//MARK: ADD THE CHILD
private func add(asChildViewController viewController: UIViewController) {
containerView.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = containerView.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParent: self)
}
Try to set your child view controller constraints relative to the container view.
Edit your add method like this:
private func add(asChildViewController viewController: UIViewController)
{
viewController.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(viewController.view)
viewController.view.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
viewController.view.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
// Notify Child View Controller
viewController.didMove(toParent: self)
}
Also you can create an extension to UIView class to pin the view to the parent view by just one line code
extension UIView {
func pin(to superview: UIView){
translatesAutoresizingMaskIntoConstraints = false
topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
leadingAnchor.constraint(equalTo: superview.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: superview.trailingAnchor).isActive = true
bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
}
}
put this in a separate swift file with this method you can make the container view same size as the view controller
containerView.addSubview(viewController.view)
viewController.view.pin(containerView)
This will save you a lot of time in the future.

Simple NSPageController example throws an unknown subview warning and stops working

I'm trying to get a very basic NSPageController to work (in book mode, not history mode). It will successfully transition once, and then stop working.
I suspect I'm creating the NSImageViews I'm loading into it wrong, but I can't figure out how.
The storyboard has a the SamplePageController which holds in initial hard-coded NSImageView.
I suspect I'm missing something really obvious here, since all of the tutorial's I've found for NSPageController are in Objective C not swift, and tend to focus on the history view mode.
The code is:
import Cocoa
class SamplePageController: NSPageController, NSPageControllerDelegate {
private var images = [NSImage]()
#IBOutlet weak var Image: NSImageView!
//Gets an object from arranged objects
func pageController(pageController: NSPageController, identifierForObject object: AnyObject) -> String {
let image = object as! NSImage
let image_name = image.name()!
let temp = arrangedObjects.indexOf({$0.name == image_name})
return "\(temp!)"
}
func pageController(pageController: NSPageController, viewControllerForIdentifier identifier: String) -> NSViewController {
let controller = NSViewController()
let imageView = NSImageView(frame: Image.frame)
let intid = Int(identifier)
let intid_u = intid!
imageView.image = images[intid_u]
imageView.sizeToFit()
controller.view = imageView
return controller
// Does this eventually lose the frame since we're returning the new view and then not storing it and the original ImageView is long gone by then?
// Alternatively, are we not sizing the imageView appropriately?
}
override func viewDidLoad() {
super.viewDidLoad()
images.append(NSImage(named:"text")!)
images.append(NSImage(named:"text-2")!)
arrangedObjects = images
delegate = self
}
}
In this case your pageController.view is set to your window.contentView and that triggers the warning. What you need to do is add a subview in the window.contentView and have your pageController.view point to that instead.
The reason for the warning is that since NSPageController creates snapshots (views) of your content history, it will add them at the same level as your pageController.view to transition between them: that means it will try to add them to pageController.view.superview.
And if your pageController.view is set to window.contentView, you are adding subviews to the window.contentView.superview, which is not supported:
New since WWDC seed: NSWindow has never supported clients adding subviews to anything other than the contentView.
Some applications would add subviews to the contentView.superview (also known as the border view of the window). NSWindow will now log when it detects this scenario: "NSWindow warning: adding an unknown subview:".
Applications doing this will need to fix this problem, as it prevents new features on 10.10 from working properly. See titlebarAccessoryViewControllers for official API.