How to create the equivalent of a SwiftUI stack view spacer programmatically - objective-c

I'm working on trying to recreate the first SwiftUI Tutorial in code without using SwiftUI. In the example, it mentions using a Spacer "to direct the layout to use the full width of the device":
VStack {
Text("Turtle Rock")
HStack {
Text("Joshua Tree National Park")
Spacer()
Text("California")
}
}
For the VStack, HStack, and Text views, I can easily use UIStackView and UILabel. But for the Spacer, I can't seem to find any equivalent in the standard library (no UISpacer or anything like that), which makes me think this is something custom to SwiftUI. In the tutorial, it describes how this Spacer works:
A spacer expands to make its containing view use all of the space of its parent view, instead of having its size defined only by its contents.
So how can I recreate the functionality of this Spacer view programmatically? Do I just add a constraint to the UIStackView to make it full width? Or is there a way to add a subview to the UIStackView that makes it behave like it does in SwiftUI?

I have worked solution using dummy UIView() which has set big constant widthConstraint using low priority - this ensures UIView will grow as much as possible but will not grow over superview constraints since it has lower priority.
Example:
let titleLabel = UILabel()
titleLabel.text = "Joshua Tree National Park"
let spacer = UIView()
// maximum width constraint
let spacerWidthConstraint = spacer.widthAnchor.constraint(equalToConstant: .greatestFiniteMagnitude) // or some very high constant
spacerWidthConstraint.priority = .defaultLow // ensures it will not "overgrow"
spacerWidthConstraint.isActive = true
let descriptionLabel = UILabel()
descriptionLabel.text = "California"
UIStackView(arrangedSubviews: [titleLabel, spacer, descriptionLabel])

Swift 5
You can create an extension for UIView to create a spacer in this way:
extension UIView {
static func spacer(size: CGFloat = 10, for layout: NSLayoutConstraint.Axis = .horizontal) -> UIView {
let spacer = UIView()
if layout == .horizontal {
spacer.widthAnchor.constraint(equalToConstant: size).isActive = true
} else {
spacer.heightAnchor.constraint(equalToConstant: size).isActive = true
}
return spacer
}
}
After that, you can add your spacer into stacks in this way:
// ... some code
let stack = UIStackView(arrangedSubviews: [UIView.spacer(), UILabel(), UIView.spacer(), UILabel(), UIView.spacer()])
stack.axis = .horizontal
stack.distribution = .equalSpacing
// For vertical stacks
let stack = UIStackView(arrangedSubviews: [UIView.spacer(for: .vertical), UILabel(), UIView.spacer(for: .vertical), UILabel(), UIView.spacer(for: .vertical)])
stack.axis = .vertical
stack.distribution = .equalSpacing
I think in this way, you can play with spacer size/layout and stack distribution/alignment to get the desired space.

A plain UIView() can replace a SwiftUI.Spacer as long as distribution is set to fill, since it has no set width constraint. I've only tested this vertically. However, you should note the layout performed by UIStackView and SwiftUI.HStack are not actually the same steps, though the end results may appear similar; something to look into.

Related

How to make the performance of NSTextField consistent with NSAttributedString?

We have an NSView that uses quartz's related methods to draw pictures and text, and we can control scale and translate.
It is similar to a vector graphics software. After double clicking the text layer, you can input text and display it in real time.
Now we need to add a requirement. Double click the text in the fixed area to freely edit and reuse NSAttributedString to draw on NSView. Therefore, we use NSTextField as a sub view of NSView.
But we don't know how to make NSTextField behave like NSAttributedString, because we want it to overlap with the text on NSView.
The following enables us to generate the component configuration code and result diagram.
/// setup
func setup(){
let rect = tree.convertToRectOnView(rect: node.drawCellRect)
textField.setFrameSize(rect.size)
textField.setFrameOrigin(rect.origin)
textField.isEditable = true
textField.isSelectable = true
let newFont = self.font.withSize(self.font.pointSize * self.zoomScale)
let ps = NSMutableParagraphStyle()
ps.minimumLineHeight = newFont.ascender + abs(newFont.descender) + abs(newFont.leading)
ps.maximumLineHeight = newFont.ascender + abs(newFont.descender) + abs(newFont.leading)
textField.font = newFont
textField.attributedStringValue = NSAttributedString(string: "title", attributes: [.font: newFont, .paragraphStyle: ps])
}

UIImageView is not scrolling inside a scrollView

I am creating a scrollview with a page control. I have a scrollview added to my viewcontroller and inside the scrollview there is a UIImageView. But for some reason I cannot scroll it to the left or right side. Can anyone help me out to fix this problem?
My Code:
var tutorialImages: [String] = ["1", "2", "3"]
var frame = CGRect.zero
func pageControll() {
pageControllTutorial.numberOfPages = tutorialImages.count
for i in 0..<tutorialImages.count {
imageViewTutorial.frame = frame
imageViewTutorial.image = UIImage(named: tutorialImages[i])
scrollViewTutorial.addSubview(imageViewTutorial)
}
scrollViewTutorial.contentSize = CGSize(width: imageViewTutorial.frame.size.width * CGFloat(tutorialImages.count), height: imageViewTutorial.frame.size.height)
scrollViewTutorial.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControllTutorial.currentPage = Int(pageNumber)
}
With the little bit of code you've shown, you're doing a number of things wrong.
Based on this line:
imageViewTutorial.image = UIImage(named: tutorialImages[I])
it looks like you have one UIImageView and you are setting its .image property 3 times, instead of creating 3 different image views.
Also, there is nothing in your code indicating how you are setting the frames of the image views.
I highly recommend using auto-layout instead of explicit frames - makes things much, much easier going forward.
Here is a complete example. It will create a square (1:1 ratio) scroll view 20-pts from the top with 20-pts padding on each side, and a UIPageControl below. It then adds a horizontal UIStackView to the scroll view. That stack view will hold the image views. Once the image views are added, the stack view will automatically define the "scrollable area" -- no need for calculating .contentSize.
Here's what it will look like:
Everything is done in code, so just assign the class of an empty view controller to SlidesExampleViewController ... no #IBOutlet or #IBAction connections needed.
class SlidesExampleViewController: UIViewController, UIScrollViewDelegate {
lazy var pageControl: UIPageControl = {
let v = UIPageControl()
v.backgroundColor = .brown
return v
}()
lazy var scrollView: UIScrollView = {
let v = UIScrollView(frame: .zero)
v.backgroundColor = .yellow
v.delegate = self
return v
}()
lazy var stackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fill
v.spacing = 0
return v
}()
let tutorialImages: [String] = ["1", "2", "3"]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
scrollView.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
pageControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.addSubview(pageControl)
scrollView.addSubview(stackView)
let v = view.safeAreaLayoutGuide
let g = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view to top, leading, trailing with 20-pts "padding"
scrollView.topAnchor.constraint(equalTo: v.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -20.0),
// constrain scroll view height equal to scroll view width
scrollView.heightAnchor.constraint(equalTo: scrollView.widthAnchor),
// constrain stack view to all 4 sides of scroll view's contentLayoutGuide
stackView.topAnchor.constraint(equalTo: g.topAnchor),
stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain stack view height equal to scroll view height
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor),
// constrain page control width to 80% of scroll view width
pageControl.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.8),
// constrain page control top to 8-pts below scroll view bottom
pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
// constrain page control centerX to centerX of scroll view
pageControl.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor, constant: 0.0),
])
tutorialImages.forEach { name in
if let img = UIImage(named: name) {
let imgView = UIImageView(image: img)
stackView.addArrangedSubview(imgView)
imgView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
}
}
pageControl.numberOfPages = tutorialImages.count
scrollView.isPagingEnabled = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
pageControl.currentPage = Int(pageIndex)
}
}
Increase the contentSize of scroll view As (number of images * width of image) expecting all image are equal in size.
int startPosition = 0;
set the image origin.x
1 image1.frame.origin.x = startPosition;
startPosition = startPosition + image1.frame.size.width;
2 image2.frame.origin.x = startPosition;
startPosition = startPosition + image2.frame.size.width;
3 image3.frame.origin.x = startPosition;
startPosition = startPosition + image3.frame.size.width;
or either you can put these in a for loop to set the frame size of image.
The frame of each UIImageView is being set to CGRect.zero so they all have a 0 height and width, which means your scroll views contentSize is also 0x0. As far as the scroll view is concerned, there's nothing to scroll here.
Do this instead:
let image = UIImage(named: tutorialImages[i])
imageViewTutorial.frame = CGRect(x: widthSoFar, y: 0, width: image.width, height: image.height)
imageViewTutorial.image = image
scrollViewTutorial.addSubview(imageViewTutorial)
widthSoFar += image.size.width
Initialize widthSoFar outside the for loop to 0.
Always Keep the
Scroll view size is greater or equal to Imageview size.
check and keep UIImageView userInteraction Enable.
keep scroll view Content size as big as your number of image like for four
image in scoll view then (imageview.frame.size.width*4)
Content size = CGSize(imageview.frame.size.width*4,imageview.frame.size.height).

Recognize object inside a layer in Swift

How do I recognize an object inside shapeLayer from UIBeizierPath in Swift? Below is the example app PopAGraph. Thanks in advance.
https://vimeo.com/61877513
My code:
fileprivate func addNewPathToImage() {
path.lineJoinStyle = CGLineJoin.round
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = UIColor(red: 52/255, green: 152/255, blue: 219/255, alpha: 0.5).cgColor
shapeLayer.lineWidth = lineWidth
mainImageView.layer.addSublayer(shapeLayer)
}
My image and shapeLayer:
CAShapeLayer his a hitTest() function. In your superview:
private func setUpShapeLayer() {
self.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer()
tapGesture.numberOfTapsRequired = 1
tapGesture.addTarget(self, action: #selector(viewTapped))
self.addGestureRecognizer(tapGesture)
[your code to add shapeLayer]
}
internal func viewTapped(_ recognizer:UITapGestureRecognizer) {
let p = recognizer.location(in: self)
if (shapeLayer.hitTest(p) != nil) {
[do something here]
} else if (greenLayer.hitTest(p) != nil) {
}
EDIT: As mentioned in the comments, I totally misunderstood what the question was.
The link in the OP is to an app that looks to have some proprietary functions happening. If you wish for similar iOS "out of box" functionality, here's about the best you can do:
(1) Have the user create a CAShapeLayer by panning the outline.
(2) Run a CoreImage filter (CIEdges) on the image to detect all edges.
(3) Find the "best fit" of contiguous (closed with no endpoints) edges where the hitTest() of said edges lay within the CAShapeLayer and highlight it.
I've seen this video a couple of times, and it has me wondering - what if the user traced a shape that cut off the Statue of Liberty head? What would the app do? Cut off the statue head, recognize the full statue, or result in no recognition?
This last observation isn't meant to be opinion, but instead point out that this may be a demo that shows the "best case" features of the app. And if the user does something "less than optimal" or unexpected may in fact give you a better idea of how it accomplishes what it does.

Preventing SKSpriteNode from going off screen

I have a SKSpriteNode that moves with the accelerometer by using the following code:
-(void)processUserMotionForUpdate:(NSTimeInterval)currentTime {
SKSpriteNode* ship = (SKSpriteNode*)[self childNodeWithName:#"fishderp"];
CMAccelerometerData* data = self.motionManager.accelerometerData;
if (fabs(data.acceleration.y) > 0.2) {
[gameFish.physicsBody applyForce:CGVectorMake(0, data.acceleration.y)];
}
}
This works well however, the node (gamefish) moves off the screen. How can I prevent this and have it stay on the screen?
Try using an SKConstraint which was designed exactly for this purpose and introduced in iOS8:
Just add this to the setup method of the gameFish node. The game engine will apply the constraint after the physics has run. You won't have to worry about it. Cool huh?
// get the screensize
CGSize scr = self.scene.frame.size;
// setup a position constraint
SKConstraint *c = [SKConstraint
positionX:[SKRange rangeWithLowerLimit:0 upperLimit:scr.width]
Y:[SKRange rangeWithLowerLimit:0 upperLimit:scr.width]];
gameFish.constraints = #[c]; // can take an array of constraints
The code depends on whether you have added the gameFish node to self or to another node (something like a "worldNode"). If you have added it to self, look at the code below:
// get the screen height as you are only changing your node's y
float myHeight = self.view.frame.size.height;
// next check your node's y coordinate against the screen y range
// and adjust y if required
if(gameFish.position.y > myHeight) {
gameFish.position = CGPointMake(gameFish.position.x, myHeight);
}
For the bottom you can do a check of < 0 or whatever value you need.

How to toggle visibility of NSSplitView subView + hide Pane Splitter divider?

We have a parent Split view (NSSplitView), and two subviews, Content and SideBar (the sidebar is on the right).
What would be the optimal Cocoa-friendly way to toggle the SideBar view?
I would really love it, if the suggested solution includes animation
I really don't need any suggestions related to external plugins, etc (e.g. BWToolkit)
HINT : I've been trying to do that, but still I had issues hiding the divider of the NSSplitView as well. How could I do it, while hiding it at the same time?
Here's a pretty decent tutorial that shows how to do this: Unraveling the Mysteries of NSSplitView.
Hiding the divider is done in NSSplitView's delegate method splitView:shouldHideDividerAtIndex:.
You will have to animate the frame size change yourself if you don't like the way NSSplitView does it.
Easiest way to do it is as follows - and it's animated: [SWIFT 5]
splitViewItems[1].animator().isCollapsed = true // Show side pane
splitViewItems[1].animator().isCollapsed = false // hide side pane
I wrote a Swift version of the content in the link from #Nathan's answer that works for me. In the context of my example splitView is set elsewhere, probably as an instance property on an encompassing class:
func toggleSidebar () {
if splitView.isSubviewCollapsed(splitView.subviews[1] as NSView) {
openSidebar()
} else {
closeSidebar()
}
}
func closeSidebar () {
let mainView = splitView.subviews[0] as NSView
let sidepanel = splitView.subviews[1] as NSView
sidepanel.hidden = true
let viewFrame = splitView.frame
mainView.frame.size = NSMakeSize(viewFrame.size.width, viewFrame.size.height)
splitView.display()
}
func openSidebar () {
let sidepanel = splitView.subviews[1] as NSView
sidepanel.hidden = false
let viewFrame = splitView.frame
sidepanel.frame.size = NSMakeSize(viewFrame.size.width, 200)
splitView.display()
}
These functions will probably methods in a class, they are for me. If your splitView can be nil you obviously have to check for that. This also assumes you have two subviews and the one at index 1, here as sidePanel is the one you want to collapse.
In Xcode 9.0 with Storyboards open Application Scene select View->Menu->Show sidebar. CTRL-click Show Sidebar, in sent actions delete the provided one, click on x. From the circle CTRL drag to First Responder in application scene and select toggleSideBar to connect to. Open storyboard and select the first split view item and in attributes inspector change behaviour from default to sidebar. Run and try with view menu item show/hide. All done in interface builder no code. toggleSideBar handles the first split view item. https://github.com/Dis3buted/SplitViewController
I got some artifacts with the code above, likely because it was out of context. I am sure it works where it was meant to. Anyway, here is a very streamlined implementation:
// this is the declaration of a left vertical subview of
// 'splitViewController', which is the name of the split view's outlet
var leftView: NSView {
return self.splitViewController.subviews[0] as NSView
}
// here is the action of a button that toggles the left vertical subview
// the left subview is always restored to 100 pixels here
#IBAction func someButton(sender: AnyObject) {
if splitViewController.isSubviewCollapsed(leftView) {
splitViewController.setPosition(100, ofDividerAtIndex: 0)
leftView.hidden = false
} else {
splitViewController.setPosition(0, ofDividerAtIndex: 0)
leftView.hidden = true
}
}
To see a good example using animations, control-click to download this file.
If your NSSplitView control is part of a NSSplitViewController object, then you can simply use this:
splitViewController.toggleSidebar(nil)