How to make an Xcode playground rotate to landscape - swift

I am trying to test a landscape view, but so far I cannot make progress. My code looks like this:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.text = "Hallo"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
self.view = view
NSLayoutConstraint.activate([label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor)])
simulateDeviceRotation(toOrientation: .landscapeLeft)
}
override func viewDidAppear(_ animated: Bool) {
//simulateDeviceRotation(toOrientation: .landscapeLeft)
}
}
// Present the view controller in the Live View window
func simulateDeviceRotation(toOrientation orientation: UIDeviceOrientation) {
let orientationValue = NSNumber(value: orientation.rawValue)
UIDevice.current.setValue(orientationValue, forKey: "orientation")
}
PlaygroundPage.current.liveView = MyViewController()
When I uncomment the call to simulateDeviceRotation(toOrientation:) in loadView(), the result is:
Rotation in loadView()
And, when I uncomment simulateDeviceRotation(toOrientation:) in viewDidAppear(_:), the result is:
rotation in viewDidAppear(_:)
Of course, I would like to see the second result, but with the horizontal rotation of the first. Can you please point me in the right direction? I am missing something but I have not been able to finde it.

Don't mess with orientation, you won't succeed.
Change the last line of your code to:
var myViewController = MyViewController()
myViewController.preferredContentSize = CGSize(width: 668, height: 375)
PlaygroundPage.current.liveView = myViewController
and remove everything that handles with device orientation in loadView, viewDidAppear and remove method simulateDeviceRotation.
Update
If you set an arbitrary value as preferredContentSize, you will get into problems with constraints or worse like the view is displaced in the live view.
What I did: first read the default values of current content size:
print(myViewController.preferredContentSize)
This was 375.0 as width and 668.0 as height for me.
So just swap this values for the new preferredContentSize and everything should be fine.

Related

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

Swift: Make the background image of UITableViewController embedded in Navigation Controller fill the entire screen

I managed to create translucent and rounded UITableViewCells in a UITableViewController that is embedded inside a Navigation Controller with this line of code in viewDidLoad():
tableView.backgroundView = UIImageView(image: UIImage(named: "nightTokyo"))
But I want the background image to fill the entire phone screen. I changed the code (and only this line of code) to:
navigationController?.view = UIImageView(image: UIImage(named: "nightTokyo"))
Now the background image fills up the entire phone screen, but my table and even the iPhone's time and battery indicator icons are missing.
What I want is for the background image to fill the entire screen, but the tableView, its cells, the iPhone time, battery level icon, etc. to remain displayed.
navigationController?.setNavigationBarHidden(true, animated: true)
Here is what I did which worked for me using Swift 5, XCode 12.
Step 1 (Optional) - Create a custom UINavigationController class
class CustomNavigationController: UINavigationController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationBar.isTranslucent = true
}
Replace your UINavigationController with this UINavigationController subclass. I mark this as optional as this is based on preference, if you do not set this, your navigation bar will be opaque and you cannot see what's beneath it.
Setting the navigationBar.isTranslucent = true allows you to see the background beneath it which is what I like. A subclass is also optional but you might need to make other updates to your nav bar so I always like to make this a subclass.
Step 2 - Set up your background view constraints
class CustomViewController: UIViewController {
// your background view
let bgImageView: UIImageView = {
let bgImageView = UIImageView()
bgImageView.image = UIImage(named: "gradient_background")
bgImageView.contentMode = .scaleAspectFill
return bgImageView
}()
// Get the height of the nav bar and the status bar so you
// know how far up your background needs to go
var topBarHeight: CGFloat {
var top = self.navigationController?.navigationBar.frame.height ?? 0.0
if #available(iOS 13.0, *) {
top += UIApplication.shared.windows.first?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
} else {
top += UIApplication.shared.statusBarFrame.height
}
return top
}
var isLayoutConfigured = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
title = "Site Visit"
// you only want to do this once
if !isLayoutConfigured() {
isLayoutConfigured = true
configBackground()
}
}
private func configBackground() {
view.addSubview(bgImageView)
configureBackgroundConstraints()
}
// Set up your constraints, main one here is the top constraint
private func configureBackgroundConstraints() {
bgImageView.translatesAutoresizingMaskIntoConstraints = false
bgImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: -topBarHeight).isActive = true
bgImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 0).isActive = true
bgImageView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
bgImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: 0).isActive = true
view.layoutIfNeeded()
}
Before setting constraints:
After setting above constraints:

SwiftUI View and UIHostingController in UIScrollView breaks scrolling

When I add a UIHostingController which contains a SwiftUI view as a childView, and then place that childView inside a UIScrollView, scrolling breaks.
Here I have my View
struct TestHeightView: View {
let color: UIColor
var body: some View {
VStack {
Text("THIS IS MY TEST")
.frame(height: 90)
}
.fixedSize(horizontal: false, vertical: true)
.background(Color(color))
.edgesIgnoringSafeArea(.all)
}
}
Then I have a UIViewController with a UIScrollView as the subView. Inside the UIScrollView there is a UIStackView that is correctly setup to allow loading UIViews and scrolling through them if the stack height becomes great enough. This works. If I were to load in 40 UILabels, it would scroll through them perfectly.
The problem arises when I add a plain old UIView, and then add a UIHostingController inside that container. I do so like this:
let container = UIView()
container.backgroundColor = color.0
stackView.insertArrangedSubview(container, at: 0)
let test = TestHeightView(color: color.1)
let vc = UIHostingController(rootView: test)
vc.view.backgroundColor = .clear
add(child: vc, in: container)
func add(child: UIViewController, in container: UIView) {
addChild(child)
container.addSubview(child.view)
child.view.translatesAutoresizingMaskIntoConstraints = false
child.view.topAnchor.constraint(equalTo: container.topAnchor, constant: 0).isActive = true
child.view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 0).isActive = true
child.view.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0).isActive = true
child.view.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0).isActive = true
child.didMove(toParent: self)
}
In my example I added 3 of these containerViews/UIHostingController and then one UIView (green) to demonstrate what is happening.
You can see that as I scroll, all views are suspended as a gap is formed. What is happening is that the containing UIView (light color) is expanding its height. Once the height reaches a certain value, scrolling continues as normal until the next container/UIHostingController reaches the top and it begins again.
I have worked on several different solutions
.edgesIgnoringSafeArea(.all)
Does do something. I included it in my example because without it, the problem is exactly the same only more jarring and harder to explain using a video. Basically the same thing happens but without any animation, it just appears that the UIScrollView has stopped working, and then it works again
Edit:
I added another UIViewController just to make sure it wasn't children in general causing the issue. Nope. Only UIHostingControllers do this. Something in SwiftUI
Unbelievably this is the only answer I can come up with:
I found it on Twitter here https://twitter.com/b3ll/status/1193747288302075906?s=20 by Adam Bell
class EMHostingController<Content> : UIHostingController<Content> where Content : View {
func fixedSafeAreaInsets() {
guard let _class = view?.classForCoder else { return }
let safeAreaInsets: #convention(block) (AnyObject) -> UIEdgeInsets = { (sself : AnyObject!) -> UIEdgeInsets in
return .zero
}
guard let method = class_getInstanceMethod(_class.self, #selector(getter: UIView.safeAreaInsets)) else { return }
class_replaceMethod(_class, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
let safeAreaLayoutGuide: #convention(block) (AnyObject) ->UILayoutGuide? = { (sself: AnyObject!) -> UILayoutGuide? in
return nil
}
guard let method2 = class_getInstanceMethod(_class.self, #selector(getter: UIView.safeAreaLayoutGuide)) else { return }
class_replaceMethod(_class, #selector(getter: UIView.safeAreaLayoutGuide), imp_implementationWithBlock(safeAreaLayoutGuide), method_getTypeEncoding(method2))
}
override var prefersStatusBarHidden: Bool {
return true
}
}
Had the same issue recently, also confirm that safe area insets are breaking the scrolling. My fix on iOS 14+ with the ignoresSafeArea modifier:
public var body: some View {
if #available(iOS 14.0, *) {
contentView
.ignoresSafeArea()
} else {
contentView
}
}
I had a very similar issue and found a fix by adding the following to my UIHostingController subclass:
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = []
}

Why is this Lottie JSON animation not showing in the UIView upon building?

This is my UIViewController:
import UIKit
import Lottie
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad() {
let rocketAnimationView = AnimationView()
//Set animation
let rocketAnimation = Animation.named("my-json-rocket-animation")
rocketAnimationView.animation = rocketAnimation
rocketAnimationView.play()
}
When I build, the animation does not show--the view just remains blank. I have tried implementing the following in viewDidLoad(), which seems to work, but my animation does not scale to my entire view:
let animation = Animation.named("my-json-rocket-animation")
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
view.addSubview(animationView)
animationView.backgroundBehavior = .pauseAndRestore
animationView.translatesAutoresizingMaskIntoConstraints = false
animationView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true
animationView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
animationView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
animationView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
animationView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
I expected my animation to display in the view, but my screen remains blank. Do I need to create a new UIView and connect it with an IBOutlet, of class type AnimationView, and then somehow set the animation? I know this was possible by simply writing animationView.setAnimation(named: "my-json-rocket-animation") prior to version 3.1.2, but I'm not sure if it is still possible.
I'll post my solution for anyone who has trouble with this in the future, as I think Airbnb's documentation is a little vague regarding setting an animation to a UIView. The issue was that I essentially forgot to set the view's frame size, and I had to use an IBOutlet corresponding to the UIView.
import UIKit
import Lottie
class ViewController: UIViewController {
#IBOutlet weak var lottieView: UIView!
let animationView = AnimationView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
animationView.animation = Animation.named("my-json-rocket-animation")
animationView.frame.size = lottieView.frame.size
animationView.contentMode = .scaleToFill
lottieView.addSubview(animationView)
animationView.backgroundBehavior = .pauseAndRestore
animationView.play()
}
}

Add view over tableview (UITableViewController)

Situation: I've got a UITableViewController loading some data asynchronously from a service. During this time I would like to place a full screen (except navigation bar) view over the table view showing my custom indicator and text.
Problem: The problem I'm facing is that when my custom view (it has a red background) is placed over the UITableView the lines of the table view are shown trough my custom view (see image below).
What I tried:
I tried to use insertBelow and above, didn't work. I also tried to do: tableview.Hidden = true, but this also hides the custom view for some reason as seen on image 2.
Image1: For some reason I can see the lines threw my view.
Image 2: Tableview + custom view gone when hidden = true used.
My code:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
UIView view = new UIView (new RectangleF (0, 0, this.TableView.Frame.Width, this.TableView.Frame.Height));
view.BackgroundColor = UIColor.Red;
this.TableView.AddSubview (view);
TableView.Source = new SessionTableViewSource ();
}
You can use self.navigationController.view as view for adding subview.
The issue is that the View of a UITableViewController is a UITableView, so you cannot add subviews to the controller on top of the table.
I'd recommend switching from a UITableViewController to a simple UIViewController that contains a UITableView. This way the controller main view is a plain UIView that contains a table, and you can add subviews to the main UIView and they will be placed on top of the table view.
You can try to add the view to the window instead of nesting it in the table view like this:
UIWindow* mainWindow = [[UIApplication sharedApplication] keyWindow];
[mainWindow addSubview: overlayview];
UIWindow* window = [[UIApplication sharedApplication].delegate.window;
[window addSubview: your-overlayview];
Swift / Storyboard Solution
Note: The code below assumes one has a custom view (ratingView in my case) that is to be presented over a UITableView.
I've read many answers to this and similar questions on SO. The other answers from these sources worked to varying degrees for me (e.g.,view loaded but not shown or not accessible,...). I am using Swift 2.0+ and I am sharing the complete solution for doing this using a UITableViewController.
Create an outlet to the Navigation Bar and the view, which you want to bring over the tableview.
//MARK:Outlets
#IBOutlet weak var navBar:UINavigationBar!
#IBOutlet var ratingView: MNGStarRating!
In my case I also wanted to animate the view over the tableview so I used a class variable to hold a reference to the inflection point and a point above the scene (off-screen).
var centerYInflection:NSLayoutConstraint!
var aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)
Then in viewDidLoad I called a function (configureRatingViewAutoLayout) which configures and adds the constraints for the new view to be animated over the tableview.
func configureRatingViewAutoLayout() {
//REQUIRED
self.navBar.superview?.addSubview(self.ratingView)
var newConstraints:[NSLayoutConstraint] = []
newConstraints.append(self.ratingView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor,constant: 10))
newConstraints.append(self.ratingView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor,constant: 10))
newConstraints.append(self.ratingView.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor))
//hides the rating view above the scene
self.centerYInflection = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor, constant: self.aPointAboveScene)
//the priority must be set below 1000 if you intend to change it after it has been added to a view
self.centerYInflection.priority = 750
newConstraints.append(self.centerYInflection)
//constraints must be added to the container view of the two items
self.ratingView.superview?.addConstraints(newConstraints)
}
Nota Bene - On a UITableViewController; the self.view is the
self.tableView. They point to the same thing so I guess one could also
use the self.tableView reference above.
Sometime later... In response to a UIControl event I call this method.
#IBAction func toggleRatingView (sender:AnyObject?){
//REQUIRED
self.ratingView.superview?.layoutIfNeeded()
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.37, initialSpringVelocity: 0.99, options: [.CurveEaseOut], animations: { () -> Void in
if CGRectContainsRect(self.view.frame, self.ratingView.frame) {
//in frame ~ animate away
//I play a sound to alert the user something is happening
self.centerYInflection.constant = self.aPointAboveScene
self.centerYInflection.priority = UILayoutPriority(950)
//I disable portions of the UI
self.disableUIElements(nil)
} else {
//out of frame ~ animate in
//I play a different sound here
self.centerYInflection.constant = 0
self.centerYInflection.priority = UILayoutPriority(950)
//I enable the UI fully
self.enableUIElements(nil)
}
//REQUIRED
self.ratingView.superview?.setNeedsLayout()
self.ratingView.superview?.layoutIfNeeded()
}) { (success) -> Void in
//do something else
}
}
These helper methods can be configured to control access to elements in your scene during the presentation of the view.
func disableUIElements(sender:AnyObject?) {
//UI
}
func enableUIElements(sender:AnyObject?) {
//UI
}
Caveats
My view is a custom view in the Storyboard (sitting outside of the
tableview but connected to the TableView Controller). The view has a
required user runtime attribute defined layer.zPosition with a Number value set to 2 (this ensures that it presents in front of the
UITableView).
One could also try playing around with bringSubviewToFront:
and sendSubviewToBack: methods if you don't want to set the zPosition
(I think zPosition is simpler to use)
Try this to hook a button at bottom of the UITableViewController
declare button as a variable:
var submitButton: UIButton!
and in viewDidLoad:
submitButton = UIButton(frame: CGRect(x: 5, y: UIScreen.main.bounds.size.height - 50, width: UIScreen.main.bounds.size.width - 10, height: 50))
submitButton.backgroundColor = UIColor.init(red: 180/255, green: 40/255, blue: 56/255, alpha: 1.0)
submitButton.setTitle("Submit", for: .normal)
submitButton.titleLabel?.font = UIFont(name: "Arial", size: 15)
submitButton.titleLabel?.textColor = .white
submitButton.addTarget(self, action: #selector(submit), for: .touchUpInside)
submitButton.layer.cornerRadius = 5
self.view.addSubview(submitButton)
and implement this method:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
submitButton.frame = CGRect.init(x: submitButton.frame.origin.x, y: UIScreen.main.bounds.size.height + scrollView.contentOffset.y - 50, width: submitButton.frame.width, height: submitButton.frame.height)
}
This works for me:
if let myTopView = Bundle.main.loadNibNamed("MyTopView", owner: self, options: nil)?.first as? MyTopView {
if let view = UIApplication.shared.keyWindow{
view.addSubview(myView);
myTopView.translatesAutoresizingMaskIntoConstraints = false
myTopView.topAnchor.constraint(equalTo: view.topAnchor ).isActive = true
myTopView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myTopView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myTopView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}