Set a bool value using model object didSet - swift

I'm struggling to figure out how to properly set bool values using a model object's didSet. My app has a series of swipable cards where some flip and some don't. This code below is the CardView which is run for each card created.
Currently, the code works perfectly for the image and label—each card loads unique information based each card's model object. However, the button and isFlippable property are where I'm struggling.
The code right now is always loading the green pathway. The weird thing, however, is that even when the cardModel should sets the button isEnabled to false, it will still load the green (but the button won't work, so it did become disabled...)
var cardModel: CardModel! {
didSet {
imageView.image = cardModel.image
label.text = cardModel.label
flipButton.isEnabled = cardModel.isFlippable
isBackShowing = cardModel.isFlippable //Intentionally use isFlippable here because I want the initial layout to be based on this true or false value.
}
}
let imageView = UIImageView()
let label = UILabel()
let flipButton = UIButton()
var isBackShowing = false
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
}
fileprivate func setupLayout() {
if flipButton.isEnabled == true {
if isBackShowing == true {
backgroundColor = .red
} else {
backgroundColor = .green
}
} else {
backgroundColor = .yellow
}
}
I also have code for when the button flips that alternates "isBackShowing" and then calls setupLayout()—it is working fine. But it always loads as false during the initial setup of the card.

For better readability you can little bit update your code replacing var isBackShowing = Bool() by var isBackShowing = false.
And also you can call setupLayout() to update your layout after setting of cardModel. For example didSet of cardModel can looks like this:
var cardModel: CardModel! {
didSet {
imageView.image = cardModel.image
label.text = cardModel.label
flipButton.isEnabled = cardModel.isFlippable
isBackShowing = cardModel.isFlippable
setupLayout()
}
}

Related

UIViewRepresentable remains white

I have this very basic setup:
struct ConversationChatMessagesWrapper: UIViewRepresentable {
private let view = ConversationChatViewWithSendMessage()
func makeUIView(context _: Context) -> ConversationChatViewWithSendMessage {
view
}
func updateUIView(
_: ConversationChatViewWithSendMessage,
context _: Context
) {}
}
class ConversationChatViewWithSendMessage: UIView {
#available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("Not used")
}
init() {
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .red
let otherView = UIView()
otherView.backgroundColor = .blue
otherView.translatesAutoresizingMaskIntoConstraints = false
addSubview(otherView)
otherView.topAnchor.constraint(equalTo: topAnchor).isActive = true
otherView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
otherView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
otherView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
}
I haven't worked with UIKit in a while, but I thought it should see a blue screen, or at least a red screen, but my screen remains white. When I remove the call which adds the otherView to ConversationChatViewWithSendMessage, I see a blue screen, so something is wrong while adding otherView, but I can't see what. I even replicated this setup in Storyboard and that just worked fine.
Any suggestions?
This is a screenshot from Xcode, the warning states:
Position and size are ambigious
Remove this line after the super.init.
translatesAutoresizingMaskIntoConstraints = false

Swift: button added within UIView not clickable

I have the following container view:
class NotificationsContainer: UIView {
init() {
super.init(frame: .zero)
controller.view.translatesAutoresizingMaskIntoConstraints = false
addSubview(controller.view)
controller.view.isHidden = true
self.isUserInteractionEnabled = true
self.clipsToBounds = false
configureAutoLayout()
}
var showNotifications = false {
didSet {
if showNotifications == true {
controller.view.isHidden = false
} else {
controller.view.isHidden = true
}
}
}
internal lazy var notificationBanner: AlertView = {
let banner = AlertView()
banner.attrString = UploadNotificationManager.shared.notificationBannerText()
banner.alertType = .notification
banner.translatesAutoresizingMaskIntoConstraints = false
addSubview(banner)
banner.isUserInteractionEnabled = true
banner.showMeButton.addTarget(self, action: #selector(showHideNotifications), for: .touchDown)
return banner
}()
#objc func showHideNotifications() {
showNotifications = showNotifications == false ? true : false
}
private lazy var notificationView: NotificationContentView = {
let notificationView = NotificationContentView()
return notificationView
}()
private lazy var controller: UIHostingController = {
return UIHostingController(rootView: notificationView)
}()
private func configureAutoLayout() {
NSLayoutConstraint.activate([
notificationBanner.leadingAnchor.constraint(equalTo: leadingAnchor),
notificationBanner.trailingAnchor.constraint(equalTo: trailingAnchor),
controller.view.trailingAnchor.constraint(equalTo: notificationBanner.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: notificationBanner.bottomAnchor)
])
}
}
AlertView contains a button as follows:
internal lazy var showMeButton: UIButton = {
let button = UIButton()
button.setTitle("Show me...", for: .normal)
button.setTitleColor(UIColor.i6.blue, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: Constants.fontSize)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
Then I add the container view to my main view:
private lazy var notifications: NotificationsContainer = {
let notifications = NotificationsContainer()
notifications.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(notifications)
notifications.leadingAnchor.constraint(equalTo: flightNumber.leadingAnchor).isActive = true
notifications.trailingAnchor.constraint(equalTo: flightNumber.trailingAnchor).isActive = true
return notifications
}()
override public func viewDidLoad() {
super.viewDidLoad()
stackView.insert(arrangedSubview: notifications, atIndex: 0)
}
Now as you can see I am trying to add an action to the showMeButton. However, when I click on the button, it does nothing. I have read before that this could be to do with the frame of the container view. However, I have tried setting the height of the notification view in my main view (width should already be there due to leading and trailing constraints) and I have tried setting the height of notificationBanner as well but nothing is working.
Here is the view in the view debugger:
The showMe button does not appear to be obscured and all other views appear to have dimensions...
Look at the debug view hierarchy in Xcode and see if the view containing the button is actually showing up. You haven't set enough constraints on any of these views so the height and width look like they could be ambiguous to me. Once you're inside the view debugger, another common problem is that another invisible view is covering up the one with the button and intercepting the touch gestures.

How to create block screen with circle loader

I am doing an app that does background job that can take some time
I want to show a loader in that time
I want a black screen with a simple loader in the front of it
and show it \ hide it,
when I do actions in the background
I want to do a simple half black square with loader circle
that also blocks presses to the screen
Like in this picture:
How can I achieve that and that ?
First create one UIView which you will put in front of your LogIn view. Then add UIActivityIndicatorView to the created UIView.
let loadingIndicatorView = UIView()
let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
Now the loadingIndicatorView should have same frame size as your LogIN view. For color you can set your own color with alpha as you want to show LogIn content too. Initially keep it hidden and whenever you want to show it unhide it.
loadingIndicatorView.frame = view.frame
loadingIndicatorView.backgroundColor = .gray
loadingIndicatorView.isHidden = true
Now setup activityIndicatorView, it should be shown at centre,
activityIndicatorView.center = CGPoint(
x: UIScreen.main.bounds.size.width / 2,
y: UIScreen.main.bounds.size.height / 2
)
You can set some color to the indicator,
activityIndicatorView.color = .white
activityIndicatorView.hidesWhenStopped = true
Now add this activityIndicatorView to loadingIndicatorView and loadingIndicatorView to LogIn View.
loadingIndicatorView.addSubview(activityIndicatorView)
view.addSubview(loadingIndicatorView)
Lastly for showing do,
loadingIndicator.startAnimating()
loadingIndicatorView.isHidden = false
And for hiding,
loadingIndicator.stopAnimating()
loadingIndicatorView.isHidden = true
Updated Answer
Since the OP wanted an example code. Hence the updated answer. Hope everyone gets to learn something or the other out of it.
To start with, I created a subclass of UIView and named it PSOverlaySpinner and it looks something like below:
import UIKit
class PSOverlaySpinner: UIView {
//MARK: - Variables
private var isSpinning: Bool = false
private lazy var spinner : UIActivityIndicatorView = {
var spinner = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.white)
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.hidesWhenStopped = true
return spinner
}()
// MARK: - View Lifecycle Functions
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init() {
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.8)
self.isSpinning = false
self.isHidden = true
createSubviews()
}
deinit {
self.removeFromSuperview()
}
func createSubviews() -> Void {
self.addSubview(spinner)
setupAutoLayout()
}
// MARK: - Private Methods
private func setupAutoLayout() {
if #available(iOS 11.0, *) {
spinner.safeAreaLayoutGuide.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor).isActive = true
spinner.safeAreaLayoutGuide.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor).isActive = true
} else {
// Fallback on earlier versions
spinner.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
spinner.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
}
// MARK: - Public Methods
public func show() -> Void {
DispatchQueue.main.async {
if !self.spinner.isAnimating {
self.spinner.startAnimating()
}
self.isHidden = false
}
isSpinning = true
}
public func hide() -> Void {
DispatchQueue.main.async {
if self.spinner.isAnimating {
self.spinner.stopAnimating()
}
self.isHidden = true
}
isSpinning = false
}
}
Now move onto the ViewController that you want to add this overlay view to. Since I create my views programmatically, I will show how to do it the same way, but you can easily do it via storyboard or xibs.
Step 1 : Initialize
public lazy var spinnerView : PSOverlaySpinner = {
let loadingView : PSOverlaySpinner = PSOverlaySpinner()
return loadingView
}()
Step 2 : Add as a subview
self.view.addSubview(spinnerView)
Step 3 : Set constraints
spinnerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
spinnerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
spinnerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
spinnerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
Step 4 : To show PSOverlaySpinner
spinnerView.show()
Step 5 : To hide PSOverlaySpinner
spinnerView.hide()
That is it!!
If you want you can go ahead and modify the PSOverlaySpinner as per your needs. For example, you might want to add a UILabel below the spinner indicating him of the type of action taking place and so on.
Before
After
Old Answer
If you wish to do it manually then create a UIView with the its frame matching self.view.bounds, with 0.5-0.7 alpha and black background color. Add UIActivityIndicator as its subview constrained to its center. For a spinner specific to the image you will have to use the open sourced spinners made available. A couple of them can be found here. Once done add this view as the topmost subview in self.view.
You need to import this library SVProgressHUD and then set few properties like as follows:
SVProgressHUD.setDefaultStyle(SVProgressHUDStyle.dark)
SVProgressHUD.setBackgroundColor(.clear)
SVProgressHUD.setForegroundColor(.white)
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.show()
//SVProgressHUD.show(withStatus: "Loading something, Loading something,Loading something ...")
This will produce same UI output as needed by you in OP. You can find a running sample at my repository (TestPreLoader)

Label Colours on an #IBDesignable View appearing different of different devices

I have an #IBDesignable custom view. It contain an UIView with two subclassed UILabels. The custom subclass of UILabel is setting it's font.
What I'm trying to achieve is by changing by making the background colour of the view an inspectable property that the text colour change appropriately to be legible.
My code is below. Custom.Colour.<name> is just an enum of defined colours.
#IBDesignable
class CustomMiniView: UIView, NibLoadable {
public var view:UIView!
#IBOutlet weak var colourView: UIView!
#IBOutlet weak var headingLabel: CustomUILabel!
#IBOutlet weak var amountLabel: CustomUILabel!
#IBInspectable var blockColor:UIColor! {
didSet {
self.colourView.backgroundColor = blockColor
switch blockColor {
case Custom.Colour.darkBlue:
headingLabel.textColor = .white
amountLabel.textColor = .white
case Custom.Colour.blue:
headingLabel.textColor = .white
amountLabel.textColor = .white
case Custom.Colour.lightBlue:
headingLabel.textColor = Custom.Colour.grey
amountLabel.textColor = Custom.Colour.blue
case Custom.Colour.green:
headingLabel.textColor = .white
amountLabel.textColor = .white
case Custom.Colour.lightGreen:
headingLabel.textColor = Custom.Colour.grey
amountLabel.textColor = Custom.Colour.blue
case Custom.Colour.yellow:
headingLabel.textColor = Custom.Colour.grey
amountLabel.textColor = Custom.Colour.grey
default : printError("We have not handled text colours for a background of this colour. = \(blockColor.hexString)")
}
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setupFromNib()
self.commonInit()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupFromNib()
self.commonInit()
}
func commonInit() {
self.backgroundColor = .clear
}
}
This was working for me fine, however I was then getting report that the text was showing up as white everywhere and got sent a screen shot which confused me. When I ran this in a simulator and a different device I was able to see this not working. Here are two screen shots of what is happening on my iPad and what I expect to happen and a screen shot of what is happening on some other devices and the simulator.
This is what is happening on my device and the expected result.
This is whats happening on other devices and the incorrect result.
Is there a reason this would appeared different of different devices? I'm at a loss to the cause or how to fix this.
Thanks to comments above I've found a fix.
Rather than doing a switch on the colour I've done a switch on the .hexString of the colour which has worked.
switch blockColor.hexString {
case Custom.Colour.darkBlue.hexString :
for the record hexString is an extension to UIColor
var hexString: String {
let colorRef = cgColor.components
let r = colorRef?[0] ?? 0
let g = colorRef?[1] ?? 0
let b = ((colorRef?.count ?? 0) > 2 ? colorRef?[2] : g) ?? 0
let a = cgColor.alpha
var color = String(
format: "#%02lX%02lX%02lX",
lroundf(Float(r * 255)),
lroundf(Float(g * 255)),
lroundf(Float(b * 255))
)
if a < 1 {
color += String(format: "%02lX", lroundf(Float(a)))
}
return color
}
I'm still not sure why switching on hexString worked and the UIColor in the enum did not work consistently but thanks to comments about that helped me find a solution to fix it myself. Question for another time is why switching on UIColor would be unreliable but now back to work.

Is this officially the proper way of using get and set in Swift?

Let's say for example I want to make some kind of a radio button which keeps track of its active state and changes color when its state is changed. I want it to change color as I set the value. This is how I would implement it:
class TagButton: UIButton {
var _active: Bool = false
var active: Bool {
set(newVal){
_active = newVal
if(!newVal){
self.backgroundColor = UIColor.white //inactive
}
else {
self.backgroundColor = UIColor.red //active
}
}
get {
return _active
}
}
}
Now, I have seen some questions suggest a similar approach, but what bothers me, is whether or not this is actually intended use of Swift. I have a feeling I am inventing a bicycle here. And I could not find anything about this in official Swift documentation. Was anyone able to confirm this?
Your code looks like Objective-C. In Swift, there is no need to create the backing storage and you can use the property observer didSet to change the background color:
class TagButton: UIButton {
var active = false {
didSet {
backgroundColor = active ? .red : .white
}
}
}
Or you could use a Computed Property and not have storage for active at all:
class TagButton: UIButton {
var active: Bool {
set {
backgroundColor = newValue ? .red : .white
}
get {
return backgroundColor == .red
}
}
}
You can read more about Property Observers and Computed Properties here.