Autolayout constraint for iPhone 8 and 8+ using storyboard - xcode

How to define separate constraints for iPhone 8 and 8+?
Since both have different width. Is it possible to differentiate it via storyboard.
Basically I want to set different x and y margins for both phones.

No you can't separate layout for iPhone 8 and 8+ in storyboard.because Autolayout is based on size class and there is no size class available that separate iphone 8 and 8+.
for more info : https://medium.com/#craiggrummitt/size-classes-in-interface-builder-in-xcode-8-74f20a541195.

Custom constraint class
IBDesignable & IBInspectable
import UIKit
#IBDesignable
class MyConstraint: NSLayoutConstraint {
#IBInspectable
var iPhone8: CGFloat = 0 {
didSet {
if UIScreen.main.bounds.maxY == 667 &&
UIScreen.main.bounds.maxX == 375 {
constant = iPhone8
}
}
}
#IBInspectable
var iPhone8Plus: CGFloat = 0 {
didSet {
if UIScreen.main.bounds.maxX == 414 &&
UIScreen.main.bounds.maxY == 736 {
constant = iPhone8Plus
}
}
}
}
Set constraint custom class

Related

How to make a full-width macOS toolbar (titlebar accessory) in SwiftUI

Let’s start with a NavigationSplitView. It comes with a toggle sidebar button which is positioned and animated properly: (shown or hidden) ⨯ (fullscreen or windowed).
struct ContentView: View {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some View {
NavigationSplitView {
} detail: {
}
}
}
I need a full-width toolbar, expanding from the trailing edge of the toggle button to the window's right edge. Just like this:
Let’s use a placeholder which is expanding in both direction:
struct Toolbar: View {
var body: some View {
Color.red
}
}
The .toolbar modifier does not work, I guess that ToolbarItem does not get the size of its parent view or does not pass that to its children. So it is only 10 pt by 10 pt.
.toolbar {
ToolbarItem(placement: .principal) {
Toolbar()
}
}
My best take was to pair NSHostingView with addTitlebarAccessoryViewController:
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
let window = NSApplication.shared.windows[0]
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
let toolbar = Toolbar()
let titlebarAccessoryViewController = NSTitlebarAccessoryViewController()
let titlebarAccessoryHostingView = NSHostingView(rootView: toolbar)
titlebarAccessoryViewController.view = titlebarAccessoryHostingView
titlebarAccessoryViewController.layoutAttribute = .trailing
window.addTitlebarAccessoryViewController(titlebarAccessoryViewController)
let toggle = window.toolbar!.items[0]
let toggleView = toggle.view!
titlebarAccessoryHostingView.translatesAutoresizingMaskIntoConstraints = false
titlebarAccessoryHostingView.leadingAnchor.constraint(equalTo: toggleView.trailingAnchor).isActive = true
}
}
It seems the auto layout constraint is buggy because the following runtime exception is thrown:
2023-01-13 14:09:41.128026+0700 FullWidthToolbar[40901:1231494] *** Assertion failure in -[NSTitlebarAccessoryViewController _auxiliaryViewFrameChanged:], NSTitlebarAccessoryViewController.m:341
2023-01-13 14:09:41.128277+0700 FullWidthToolbar[40901:1231494] [General] changing the view's origin is not allowed
2023-01-13 14:09:41.132215+0700 FullWidthToolbar[40901:1231494] [General] (
0 CoreFoundation 0x00000001862e33e8 __exceptionPreprocess + 176
1 libobjc.A.dylib 0x0000000185e2eea8 objc_exception_throw + 60
2 Foundation 0x0000000187226910 -[NSCalendarDate initWithCoder:] + 0
3 AppKit 0x000000018954f764 -[NSTitlebarAccessoryViewController _auxiliaryViewFrameChanged:] + 376
Is this approach promising? How to fix the auto layout constraint?
I’ve tried to set the size of the toolbar using a GeometryReader, unfortunately, it makes the (show/hide sidebar, fullscreen enter/exit, resizing, etc.) animation glitchy.

NSTextField Padding on the Left

How do you add padding to the left of the text in a text field cell using swift? Previous answers are only for UITextField or in Objective C. To be clear, this is for an NSTextField.
Here is an example of someone who has made a custom NSTextFieldCell in Objective C.
Ported to Swift that looks like this:
import Cocoa
class PaddedTextFieldCell: NSTextFieldCell {
#IBInspectable var leftPadding: CGFloat = 10.0
override func drawingRect(forBounds rect: NSRect) -> NSRect {
let rectInset = NSMakeRect(rect.origin.x + leftPadding, rect.origin.y, rect.size.width - leftPadding, rect.size.height)
return super.drawingRect(forBounds: rectInset)
}
}
I've added the padding as a #IBInspectable property. That way you can set it as you like in Interface Builder.
Use With Interface Builder
To use your new PaddedTextFieldCell you drag a regular Text Field to your xib file
and then change the class of the inner TextFieldCell to be PaddedTextFieldCell
Success!
Use From Code
To use the PaddedTextFieldCell from code, you could do something like this (thank you to #Sentry.co for assistance):
class ViewController: NSViewController {
#IBOutlet weak var textField: NSTextField! {
didSet {
let paddedTextField = PaddedTextFieldCell()
paddedTextField.leftPadding = 40
textField.cell = paddedTextField
textField.isBordered = true
textField.isEditable = true
}
}
....
}
Hope that helps you.

Referencing self.frame in let statements in UIView

In ObjC, for iOS, I used to use a bunch of #define statements at the top of a class file to set constants for view layout. Usually, they referenced self.frame. I'm trying to do the same with let statements in Swift, and it won't let me use self.frame anywhere. I get errors because the frame property cannot be found:
import UIKit
class MySubview: UIView {
/* Constants */
let frameX = (self.frame.origin.x) // error
let frameY = (self.frame.origin.y) // error
let frameW = (self.frame.size.width) // error
let frameH = (self.frame.size.height) // error
// ...
}
Why is it complaining? I'm subclassing UIView, so the property should be there. How do I fix this?
What you have there are in fact not constants because the frame can change. The value of a constant property (using let) is set in the initialisation stage before access to self is allowed, and cannot be changed.
In your case, you should be using computed properties:
class MySubview: UIView {
var frameX: CGFloat { return frame.minX }
var frameY: CGFloat { return frame.minY }
var frameW: CGFloat { return frame.width }
var frameH: CGFloat { return frame.height }
// ...
}
Also, note the more convenient methods on CGRect in Swift, which might make some of these convenience variables somewhat redundant.

NSImageView image aspect fill?

So I am used to UIImageView, and being able to set different ways of how its image is displayed in it. Like for example AspectFill mode etc...
I would like to accomplish the same thing using NSImageView on a mac app. Does NSImageView work similarly to UIImageView in that regard or how would I go about showing an image in an NSImageView and picking different ways of displaying that image?
You may find it much easier to subclass NSView and provide a CALayer that does the aspect fill for you. Here is what the init might look like for this NSView subclass.
- (id)initWithFrame:(NSRect)frame andImage:(NSImage*)image
{
self = [super initWithFrame:frame];
if (self) {
self.layer = [[CALayer alloc] init];
self.layer.contentsGravity = kCAGravityResizeAspectFill;
self.layer.contents = image;
self.wantsLayer = YES;
}
return self;
}
Note that the order of setting the layer, then settings wantsLayer is very important (if you set wantsLayer first, you'll get a default backing layer instead).
You could have a setImage method that simply updates the contents of the layer.
Here is what I'm using, written with Swift. This approach works well with storyboards - just use a normal NSImageView, then replace the name NSImageView in the Class box, with MyAspectFillImageNSImageView ...
open class MyAspectFillImageNSImageView : NSImageView {
open override var image: NSImage? {
set {
self.layer = CALayer()
self.layer?.contentsGravity = kCAGravityResizeAspectFill
self.layer?.contents = newValue
self.wantsLayer = true
super.image = newValue
}
get {
return super.image
}
}
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
}
//the image setter isn't called when loading from a storyboard
//manually set the image if it is already set
required public init?(coder: NSCoder) {
super.init(coder: coder)
if let theImage = image {
self.image = theImage
}
}
}
I had the same problem. I wanted to have the image to be scaled to fill but keeping the aspect ratio of the original image. Strangely, this is not as simple as it seems, and does not come out of the box with NSImageView. I wanted the NSImageView scale nicely while it resize with superview(s). I made a drop-in NSImageView subclass you can find on github: KPCScaleToFillNSImageView
You can use this: image will be force to fill the view size
( Aspect Fill )
imageView.imageScaling = .scaleAxesIndependently
( Aspect Fit )
imageView.imageScaling = .scaleProportionallyUpOrDown
( Center Top )
imageView.imageScaling = .scaleProportionallyDown
It works for me.
I was having an hard time trying to figure out how you can make an Aspect Fill Clip to Bounds :
Picture credit: https://osxentwicklerforum.de/index.php/Thread/28812-NSImageView-Scaling-Seitenverh%C3%A4ltnis/
Finally I made my own Subclass of NSImageView, hope this can help someone :
import Cocoa
#IBDesignable
class NSImageView_ScaleAspectFill: NSImageView {
#IBInspectable
var scaleAspectFill : Bool = false
override func awakeFromNib() {
// Scaling : .scaleNone mandatory
if scaleAspectFill { self.imageScaling = .scaleNone }
}
override func draw(_ dirtyRect: NSRect) {
if scaleAspectFill, let _ = self.image {
// Compute new Size
let imageViewRatio = self.image!.size.height / self.image!.size.width
let nestedImageRatio = self.bounds.size.height / self.bounds.size.width
var newWidth = self.image!.size.width
var newHeight = self.image!.size.height
if imageViewRatio > nestedImageRatio {
newWidth = self.bounds.size.width
newHeight = self.bounds.size.width * imageViewRatio
} else {
newWidth = self.bounds.size.height / imageViewRatio
newHeight = self.bounds.size.height
}
self.image!.size.width = newWidth
self.image!.size.height = newHeight
}
// Draw AFTER resizing
super.draw(dirtyRect)
}
}
Plus this is #IBDesignable so you can set it on in the StoryBoard
WARNINGS
I'm new to MacOS Swift development, I come from iOS development that's why I was surprised I couldn't find a clipToBound property, maybe it exists and I wasn't able to find it !
Regarding the code, I suspect this is consuming a lot, and also this has the side effect to modify the original image ratio over the time. This side effect seemed negligible to me.
Once again if their is a setting that allow a NSImageView to clip to bounds, please remove this answer :]
Image scalling can be updated with below function of NSImageView.
[imageView setImageScaling:NSScaleProportionally];
Here are more options to change image display property.
enum {
NSScaleProportionally = 0, // Deprecated. Use NSImageScaleProportionallyDown
NSScaleToFit, // Deprecated. Use NSImageScaleAxesIndependently
NSScaleNone // Deprecated. Use NSImageScaleNone
};
Here is another approach which uses SwiftUI under the hood
The major advantage here is that if your image has dark & light modes, then they are respected when the system appearance changes
(I couldn't get that to work with the other approaches)
This relies on an image existing in your assets with imageName
import Foundation
import AppKit
import SwiftUI
open class AspectFillImageView : NSView {
#IBInspectable
open var imageName: String?
{
didSet {
if imageName != oldValue {
insertSwiftUIImage(imageName)
}
}
}
open override func prepareForInterfaceBuilder() {
self.needsLayout = true
}
func insertSwiftUIImage(_ name:String?){
self.removeSubviews()
guard let name = name else {
return
}
let iv = Image(name).resizable().scaledToFill()
let hostView = NSHostingView(rootView:iv)
self.addSubview(hostView)
//I'm using PureLayout to pin the subview. You will have to rewrite this in your own way...
hostView.autoPinEdgesToSuperviewEdges()
}
func commonInit() {
insertSwiftUIImage(imageName)
}
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
}
Answers already given here are very good, but most of them involve subclassing NSView or NSImageView.
You could also achieve the result just by using a CALayer. But in that case you wouldn't have auto layout capabilities.
The simplest solution is to have a NSView, without subclassing it, and setting manually it's layer property. It could also be a NSImageView and achieve the same result.
Example
Using Swift
let view = NSView()
view.layer = .init() // CALayer()
view.layer?.contentsGravity = .resizeAspectFill
view.layer?.contents = image // image is a NSImage, could also be a CGImage
view.wantsLayer = true

Using a custom font in a UITextField causes it to shift slightly when accessed -- is there a fix?

I have a custom font in a UITextField, and I've noticed that when it's accessed (when the keyboard appears), the text shifts down by a very small amount -- maybe a pixel or two or three. (I've no way to measure it, of course, but it's enough for me to notice.) And then when the keyboard is dismissed, it shifts back up again.
I've tried clearing the field on editing (which hides the problem of the initial shift down), but it hasn't solved the problem. I've also looked over the various attributes in IB, and tried changing a few, but still the problem persists. I get the same results in the simulator and on my iPad2.
(The field is well clear of the keyboard, so it's not the entire view moving out of the way -- it's just the contents of that specific text field.)
I'm sure it's the custom font that's causing the problem -- it doesn't occur without it.
Any idea how to address this? I was thinking I might need to create the text field programmatically, instead of in IB -- and I know I probably ought to try that before asking the question here, but I'm loathe to go to all that trouble if it won't solve the problem.
Any advice appreciated!
I had this issue as well.
To fix, subclass UITextField and implement the following methods to adjust the positioning of text when not editing and editing.
- (CGRect)textRectForBounds:(CGRect)bounds {
return CGRectInset( bounds , 8 , 8 );
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
return CGRectInset( bounds , 8 , 5 );
}
My solution is along the same lines a McDJ's, but with a slightly different twist. Subclass UITextField and override only these:
- (CGRect)placeholderRectForBounds:(CGRect)bounds {
return CGRectOffset( [self editingRectForBounds:bounds], 0, 2 );
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
return CGRectOffset( [super editingRectForBounds:bounds], 0, -2 );
}
With the custom font I'm using, 2 points is the correct vertical adjustment, helping placeholder, "static", and "editing" text all stay on the same vertical line.
Unfortunately none of the answers worked for me.
#blackjacx answer worked but only sometimes :(
I started out debugging and here is what I've discovered:
1 - The real problem seems to be with a private subview of UITextField of type UIFieldEditorContentView
Below you can see that the y of it subview is not the same of the UITextField itself:
After realizing it I came out with the following workaround:
override func layoutSubviews() {
super.layoutSubviews()
fixMisplacedEditorContentView()
}
func fixMisplacedEditorContentView() {
if #available(iOS 10, *) {
for view in subviews {
if view.bounds.origin.y < 0 {
view.bounds.origin = CGPoint(x: view.bounds.origin.x, y: 0)
}
}
}
}
You will need to subclass UITextField and override layoutSubviews to add the ability to manually set to 0 the y of any subview that is set to a negative value. As this problem doesn't occur with iOS 9 our below I added a check to do the workaround only when it is on iOS 10.
The result you can see below:
2 - This workaround doesn't work if the user choose to select a subrange of the text (selectAll works fine)
Since the selection of the text is not a must have for my app I rather disable it. In order to do that you can use the following code (Swift 3):
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if #available(iOS 10, *) {
if action == #selector(UIResponderStandardEditActions.select(_:)) {
return false
}
}
return super.canPerformAction(action, withSender: sender)
}
Works for all font sizes and does not cause de-alignment with clearButton.
Subclass UITextField and override these as follows:
- (CGRect)placeholderRectForBounds:(CGRect)bounds
{
return CGRectOffset( bounds, 0, 4 );
}
- (CGRect)editingRectForBounds:(CGRect)bounds
{
return CGRectOffset( bounds, 0, 2);
}
- (CGRect)textRectForBounds:(CGRect)bounds
{
return CGRectOffset( bounds , 0 , 4 );
}
For a strange reason I didn't really understood I've solved this by setting automaticallyAdjustsScrollViewInsets to NO (or equivalent in Interface Builder). This with iOS 8.1.
I had this issue with a custom font and solved it by shifting the label in the other direction when the keyboard events would fire. I moved the center of the label in the button by overriding the drawRect: method
- (void)drawRect:(CGRect)rect
{
self.titleLabel.center = CGPointMake(self.titleLabel.center.x, self.titleLabel.center.y+3);
}
This is expected behaviour in a standard UITextField. You can however solve this by subclassing UITextField and by adjusting the bounds for the text itself.
Swift 3
override func textRect(forBounds bounds: CGRect) -> CGRect {
return(bounds.insetBy(dx: 0, dy: 0))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return(bounds.insetBy(dx: 0, dy: -0.5))
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return(bounds.insetBy(dx: 0, dy: 0))
}
This should do the trick!
Set your textfields Border Style to any value except "none" in IB, then, in your ViewController's viewDidLoad set:
yourTextField.borderStyle = .none
(Based on this answer by Box Jeon)
Swift 3
Do not forget the accessory views of the UITextField. You'll need to account for super of the *rect(forBounds: ...) functions if you want a working implementation. And be also sure to only displace the rects for the buggy iOS 10 and not for 9 or 8! The following code should do the trick:
public class CustomTextField: UITextField {
public override func textRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.textRect(forBounds: bounds)
if #available(iOS 10, *) {
return superValue.insetBy(dx: 0, dy: 0)
}
return superValue
}
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.editingRect(forBounds: bounds)
if #available(iOS 10, *) {
return superValue.insetBy(dx: 0, dy: -0.5)
}
return superValue
}
public override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.placeholderRect(forBounds: bounds)
if #available(iOS 10, *) {
if isEditing {
return superValue.insetBy(dx: 0, dy: 0.5)
}
return superValue.insetBy(dx: 0, dy: 0.0)
}
return superValue
}
}
EDIT
I slightly edited my code from above to the following and it works better for me. I testet it on iPhone 6, 6s, 7, 7s as well as the 'plus' devices with iOS 9.3 and 10.3.
public class CustomTextField: UITextField {
public override func textRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.textRect(forBounds: bounds)
if #available(iOS 10, *) {
return superValue.insetBy(dx: 0, dy: -0.3)
}
return superValue.insetBy(dx: 0, dy: -0.2)
}
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
return self.textRect(forBounds: bounds)
}
}
I think it also depends on the font you use. I use UIFont.systemFont(ofSize: 17.0, weight: UIFontWeightLight)
You should set Font property earlier than Text property.

Resources