I've got a UIControl class and need to do some calculation based on UIImageView location which can be moved with touchesBegan and touchesMoved (everything inside this class).
Than I would like to display it as a UILabel which I've created programmatically.
class control : UIControl{
...
let leftControl: UIImageView = UIImageView(image: UIImage(named: "left-control"))
...
func leftValue() -> String{
var leftValue : String = "0.0"
leftValue = "\(leftControl.center.x)"
return leftValue
}
}
and my ViewController.swift
class ViewController: UIViewController {
let ctrl : Control = Control()
let leftLabel : UILabel = UILabel(frame: CGRect(x: 40, y: 300, width: 150, height: 30))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
ctrl.frame.origin = CGPoint(x: 40, y: 400)
leftLabel.text = "\(ctrl.leftValue())" //displays only starting value
view.addSubview(slider)
view.addSubview(leftLabel)
view.addSubview(rightLabel)
}
I know that it's inside the viewDidLoad so it's not updating properly. I was wondering about scheduledTimer but don't know if it's good solution.
You can achieve this using protocols and delegation - in the file for your Control add this :
protocol ControlDelegate: class {
func controlPositionDidChange(leftValue: String)
}
And add a weak var delegate: ControlDelegate? inside Control class.
In the file for view controller make following changes :
class ViewController: UIViewController, ControllDelegate {
let ctrl : Control = Control()
let leftLabel : UILabel = UILabel(frame: CGRect(x: 40, y: 300, width: 150, height: 30))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
ctrl.frame.origin = CGPoint(x: 40, y: 400)
ctrl.delegate = self
leftLabel.text = "\(ctrl.leftValue())" //displays only starting value
view.addSubview(slider)
view.addSubview(leftLabel)
view.addSubview(rightLabel)
}
func controlPositionDidChange(leftValue: String) {
leftLabel.text = leftValue
}
}
Now, whenever you want to inform the delegate that your control has changed the position, simply call self.delegate?.controlPositionDidChange(self.leftValue()) in appropriate places.
As usually, there is more in the docs. I highly suggest reading through them as delegation and protocols are widely used in CocoaTouch.
The answer of #Losiowaty describes the solution that most developers choose. But there are (in my opinion) much better ways to achieve it. I prefer the object oriented solution. It might look like more code, but its a much better maintainable way with more reusable code.
A real object oriented solution of your problem might look like that:
// reusable protocol set
protocol OOString: class {
var value: String { get set }
}
// reusable functional objects
final class RuntimeString: OOString {
init(initialValue: String = "") {
self.value = initialValue
}
var value: String
}
final class ViewUpdatingString: OOString {
init(_ decorated: OOString, view: UIView) {
self.decorated = decorated
self.view = view
}
var value: String {
get {
return decorated.value
}
set(newValue) {
decorated.value = newValue
view.setNeedsLayout()
}
}
private let decorated: OOString
private let view: UIView
}
// reusable ui objects
final class MyLabel : UILabel {
init(frame: CGRect, imageXCenter: OOString) {
self.imageXCenter = imageXCenter
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not supported")
}
override func layoutSubviews() {
super.layoutSubviews()
text = imageXCenter.value
}
private let imageXCenter: OOString
}
final class MyControl : UIControl {
init(frame: CGRect, imageXCenter: OOString) {
self.imageXCenter = imageXCenter
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not supported")
}
private let imageXCenter: OOString
private let leftControl = UIImageView(image: UIImage(named: "left-control"))
// call this at change
private func updateValue() {
imageXCenter.value = "\(leftControl.center.x)"
}
}
// non reusable business logic
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let dependency = RuntimeString(initialValue: "unset")
let leftLabel = MyLabel(
frame: CGRect(x: 40, y: 300, width: 150, height: 30),
imageXCenter: dependency
)
let control = MyControl(
frame: CGRect(x: 40, y: 400, width: 400, height: 400),
imageXCenter: ViewUpdatingString(dependency, view: leftLabel)
)
view.addSubview(leftLabel)
view.addSubview(control)
}
}
The main idea is to extract the dependency of both objects into another object and use a decorator then to automatically update the ui on every set value.
Note:
this approach follows the rules of object oriented coding (clean coding, elegant objects, decorator pattern, ...)
reusable classes are very simple constructed and fullfill exactly one task
classes communicate by protocols with each other
dependencies are given by dependency injection as far as possible
internal object functionality is private (loose coupling)
everything (except the business logic) is designed for reuse -> if you code like that the portfolio of reusable code grows with every day you code
the business logic of your app is more concentrated in one place (in my real coding its even outside the UIViewController)
unittesting of reusable objects is very simple when using fake implementations for the protocols (even mocking is not needed in most cases)
lesser problems with retain cycles, in most cases you do not need weak properties any more
avoiding Null, nil and Optionals (they pollute your code)
...
Related
How to remove subviews?
I am trying to integrate GIF by creating UIView and UIImageView programmatically.
It works fine to show GIF but when the function of hiding if is called, there is no response.
Here are the codes of both functions.
class CustomLoader: UIView {
static let instance = CustomLoader()
var viewColor: UIColor = .black
var setAlpha: CGFloat = 0.5
var gifName: String = ""
lazy var transparentView: UIView = {
let transparentView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
transparentView.backgroundColor = viewColor.withAlphaComponent(setAlpha)
transparentView.isUserInteractionEnabled = false
return transparentView
}()
lazy var gifImage: UIImageView = {
var gifImage = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 60))
gifImage.contentMode = .scaleAspectFit
gifImage.center = transparentView.center
gifImage.isUserInteractionEnabled = false
gifImage.loadGif(name: gifName)
return gifImage
}()
func showLoaderView() {
self.addSubview(self.transparentView)
self.transparentView.addSubview(self.gifImage)
self.transparentView.bringSubview(toFront: self.gifImage)
UIApplication.shared.keyWindow?.addSubview(transparentView)
}
func hideLoaderView() {
self.transparentView.removeFromSuperview()
}
}
A couple of thoughts:
I’d suggest you add a breakpoint or a logging statement in hideLoaderView and make sure you’re getting to that line.
You should make the init method to this class private to make sure you’re not calling hideLoaderView on some separate instance. When dealing with singletons, you want to make sure you can’t accidentally create another instance.
But I tested your code, and it works fine. Your problem probably rests with where and how you call this (and making init private, you might find where you might be using it inappropriately).
In the comments below, you said:
I simply call the function "CustomLoader().hideLoaderView()" Both are being called technically. What do you mean by "where I using it inappropriately?"
That is the root of the problem.
The CustomLoader() of CustomLoader().hideLoaderView() will create a new instance of CustomLoader with its own transparencyView, etc., which is precisely what the problem is. You’re not hiding the old view that was presented earlier, but trying to hide another one that you just created and was never displayed.
If you instead use that static, e.g. CustomLoader.instance.showLoaderView() and CustomLoader.instance.hideLoaderView(), then the problem will go away. Then you will be hiding the same view that your previously showed.
By the way, a few other unrelated observations:
If this is a singleton or shared instance, the convention would be to call that static property shared, not instance.
By the way, you aren’t using this CustomLoader as a UIView, so I’d not make it a UIView subclass. Don’t make it a subclass of anything.
You would obviously eliminate that self.addSubview(transparentView) line, too.
The bringSubview(toFront:) call is unnecessary.
You should avoid referencing UIScreen.main.bounds. You don’t know if your app might be in multitasking mode (maybe this isn’t an issue right now, but it’s the sort of unnecessary assumption that will cause problems at some later date). Just refer to the bounds of the UIWindow to which you’re adding this. You should also update this frame when you show this view, not when you create it (in case you changed orientation in the intervening time, or whatever).
By the way, using keyWindow is discouraged in iOS 13 and later, so you might eventually want to remove that, too.
When adding the gifImage (which I’d suggest renaming to gifImageView because it’s an image view, not an image), you should not reference the center of its superview. That’s the coordinate of the transparent view in its super view’s coordinate system, which could be completely different than the transparent view’s own coordinate system. In this case, it just happens to work, but it suggests a fundamental misunderstanding of view coordinate systems. Reference the bounds of the transparentView, not its center.
If you’re going to expose viewColor and setAlpha, you should pull the setting of the transparentView’s color out of the lazy initializer and into showLoaderView, at the very least. Right now, if you show the loader once, and then change the color, and try to show it again, you won’t see the new color.
The same issue applies with the gif image. So, I’d move that to the didSet observer.
Thus, pulling this all together:
class CustomLoader{
static let shared = CustomLoader()
private init() { }
var dimmingColor: UIColor = .black
var dimmingAlpha: CGFloat = 0.5
var gifName: String = "" { didSet { gifImage.loadGif(name: gifName) } }
lazy var transparentView: UIView = {
let transparentView = UIView()
transparentView.isUserInteractionEnabled = false
return transparentView
}()
lazy var gifImageView: UIImageView = {
var gifImage = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 60))
gifImage.contentMode = .scaleAspectFit
gifImage.isUserInteractionEnabled = false
return gifImage
}()
func showLoaderView() {
guard let window = UIApplication.shared.keyWindow else { return }
transparentView.frame = window.bounds
transparentView.backgroundColor = dimmingColor.withAlphaComponent(dimmingAlpha)
gifImageView.center = CGPoint(x: transparentView.bounds.midX, y: transparentView.bounds.midY)
transparentView.addSubview(gifImageView)
window.addSubview(transparentView)
}
func hideLoaderView() {
transparentView.removeFromSuperview()
}
}
Why you are using transparentView while you are have a CustomLoader instance view
Try to use this
class CustomLoader: UIView {
static let instance = CustomLoader()
var viewColor: UIColor = .black
var setAlpha: CGFloat = 0.5
var gifName: String = ""
init() {
super.init(frame: UIScreen.main.bounds)
backgroundColor = viewColor.withAlphaComponent(setAlpha)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var gifImage: UIImageView = {
var gifImage = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 60))
gifImage.backgroundColor = .red
gifImage.contentMode = .scaleAspectFit
gifImage.center = center
gifImage.isUserInteractionEnabled = false
gifImage.loadGif(name: gifName)
return gifImage
}()
func showLoaderView() {
addSubview(self.gifImage)
UIApplication.shared.keyWindow?.addSubview(self)
}
func hideLoaderView() {
removeFromSuperview()
}
}
This might be a weird question, but i'm trying to code like a pro which obviously i am not.
Right now i have an extension which uses UIView and my concept is making it like an alert
For example, i coded the following:
extension UIView {
typealias completionHandler = (_ success:Bool) -> Void
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval) {
let luna = UIView()
luna.frame = CGRect(x: 16, y: 30, width: screenWidth - 30, height: 60)
luna.center.x = self.center.x
luna.backgroundColor = .white
luna.addShadow(radius: 11, opacity: 0.2)
luna.layer.cornerRadius = 10
}
}
And on my other ViewController, I use this to present Luna
#IBAction func presentLuna(_ sender: Any) {
self.view.showLuna(title: "Oooh", message: "Oops, something went horribly wrong!", dissmiss: 2.5);
}
At this very specific moment, I've been digging StackOverFlow for a day to find an answer. How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??
This is maybe what #rmaddy means by subclassing UIView:
First, create a subview so you can use it to respond to touch events:
class LunaView: UIView {
typealias LunaViewCompletionBlock = (_ isSuccessful: Bool) -> Void
var label: UILabel
//other things you need
var completionHandler: LunaViewCompletionBlock?
init(frame: CGRect, /* other properties such as title and colour */, completionHandler: LunaViewCompletionBlock?) {
self.completionHandler = completionHandler
self.label = UILabel()
super.init(frame: frame)
isUserInteractionEnabled = true
//this is how we handle touch
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTouch)))
addSubview(label)
// set up frame or constraints(if you use autolayout) for label and other views
// configure label.text and other things
}
func handleTouch() {
completionHandler(/*true or false*/)
}
/*other methods needed in this class*/
}
Then, to use it, simply do this in the extension:
extension UIView {
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval, completionHandler: LunaView.LunaViewCompletionBlock) {
let luna = LunaView(frame: /*size*/, /*other things like title*/, completionHandler: completionHandler)
// set luna's center, shadow, auto-dismiss time, colour.....
}
}
Answering your question:
How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??
You can't detect a touch event on just a general UIView you need a UIControl. Since the UIControl type inherits from UIView you will be able to do any of your typical drawing or view hierarchy stuff but you can also setup touch actions and callbacks using addTarget(:action:for:) or other UIControl mechanisms.
Writing an app with some network activity I find myself writing the same code for multiple view controllers over and over just to display an activity indicator.
class SomeViewController: UIViewController {
let indicator: UIActivityIndicatorView = UIActivityIndicatorView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
override func viewDidLoad() {
super.viewDidLoad()
// customize indicator
self.indicator.layer.cornerRadius = 10
self.indicator.center = self.view.center
self.indicator.hidesWhenStopped = true
self.indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.whiteLarge
self.indicator.backgroundColor = UIColor(red: 1/255, green: 1/255, blue: 1/255, alpha: 0.5)
}
// MARK: - Acitivity Indicator
func startIndicatingActivity() {
DispatchQueue.main.async {
self.view.addSubview(self.indicator)
self.indicator.startAnimating()
}
}
func stopIndicatingActivity() {
DispatchQueue.main.async {
self.indicator.stopAnimating()
}
}
}
Within the same SomeViewController class I can then use it as follows:
#IBAction func startHeavyNetworkStuffButtonPressed(_ sender: UIButton) {
startIndicatingActivity()
doHeavyNetworkStuff() { success in
// heavy networking has finished
stopIndicatingActivity()
}
}
This works fine as long as I only need to display the activity indicator in a single view controller. However, it's tedious to do it over and over for every view controller that needs this functionality. As I hate writing the same code over and over, I am in search of a solution where I can simply call
startIndicatingActivity() (and stopIndicatingActivity() respectively) in any view controller.
0th idea - Extension
My obvious first thought was to write an extension for the UIViewController class. As I need to store an instance of the UIActivityIndicatorView, however, I got the Extensions may not contain stored properties error.
1st idea - Subclassing
Next up: subclassing UIViewController. This would work fine for any simple view controller. However, if I needed the same functionality for a MyCustomTableViewController, I would again need to first subclass from UITableViewController and copy/paste existing code.
My question
Is there an elegant way to call startIndicatingActivity() / stopIndicatingActivity() in any view controller while avoiding to copy/paste large amounts of code? I'm assuming an elegant solution would involve an extension, protocol, or some kind of multiple-inheritance approach.
This SO thread is the solution! Turns out there is a way to solve this with an extension and simulated properties, after all.
Posting the complete solution for the interested reader:
Extending UIViewController
extension UIViewController {
// see ObjectAssociation<T> class below
private static let association = ObjectAssociation<UIActivityIndicatorView>()
var indicator: UIActivityIndicatorView {
set { UIViewController.association[self] = newValue }
get {
if let indicator = UIViewController.association[self] {
return indicator
} else {
UIViewController.association[self] = UIActivityIndicatorView.customIndicator(at: self.view.center)
return UIViewController.association[self]!
}
}
}
// MARK: - Acitivity Indicator
public func startIndicatingActivity() {
DispatchQueue.main.async {
self.view.addSubview(self.indicator)
self.indicator.startAnimating()
//UIApplication.shared.beginIgnoringInteractionEvents() // if desired
}
}
public func stopIndicatingActivity() {
DispatchQueue.main.async {
self.indicator.stopAnimating()
//UIApplication.shared.endIgnoringInteractionEvents()
}
}
}
Borrowing code from said SO thread
// source: https://stackoverflow.com/questions/25426780/how-to-have-stored-properties-in-swift-the-same-way-i-had-on-objective-c
public final class ObjectAssociation<T: AnyObject> {
private let policy: objc_AssociationPolicy
/// - Parameter policy: An association policy that will be used when linking objects.
public init(policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) {
self.policy = policy
}
/// Accesses associated object.
/// - Parameter index: An object whose associated object is to be accessed.
public subscript(index: AnyObject) -> T? {
get { return objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T? }
set { objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, policy) }
}
}
For the sake of completeness
I also extended the UIActivityIndicatorView class with a static function with my customizations.
extension UIActivityIndicatorView {
public static func customIndicator(at center: CGPoint) -> UIActivityIndicatorView {
let indicator = UIActivityIndicatorView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
indicator.layer.cornerRadius = 10
indicator.center = center
indicator.hidesWhenStopped = true
indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.whiteLarge
indicator.backgroundColor = UIColor(red: 1/255, green: 1/255, blue: 1/255, alpha: 0.5)
return indicator
}
}
Sample show full loading in any View
extension UIView{
/**
ShowLoader: loading view ..
- parameter Color: ActivityIndicator and view loading color .
*/
func showLoader(_ color:UIColor?){
let LoaderView = UIView(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
LoaderView.tag = -888754
LoaderView.backgroundColor = color
let Loader = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 60, height: 30))
Loader.center = LoaderView.center
Loader.activityIndicatorViewStyle = .whiteLarge
Loader.color = Color.primaryColor
Loader.startAnimating()
LoaderView.addSubview(Loader)
self.addSubview(LoaderView)
}
/**
dismissLoader: hidden loading view ..
*/
func dismissLoader(){
self.viewWithTag(-888754)?.removeFromSuperview()
}
}
call this func
in UIViewController
self.view.showLoader(nil) //you can set background color nil or any color
// dismiss
self.view.dismissLoader()
I prefer this method because you can use it any view in button ,
tableview , cell ...etc
I want to manage memory so I want to deinit the UIview when leaving ViewController.
And I try to use keyword "weak" and I get crash because my chatkeyboard is nil.
I dont know why making it crash.
Thanks.
class ChatKeyboard: UIView {
var buttonMic:UIButton = { ()->UIButton in
let ui:UIButton = GeneratorButton()
return ui
}()
override init(frame: CGRect) {
super.init(frame: frame)
print("===) ChatKeyboard init.")
translatesAutoresizingMaskIntoConstraints = false
loadContent()
loadConstrain()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
deinit {
print("===) ChatKeyboard deinit.")
}
func loadContent() {
backgroundColor = UIColor.white
addSubview(buttonMic)
}
func loadConstrain() {
buttonMic.snp.makeConstraints { (make) -> Void in
make.left.equalTo(micLeftPadding)
make.top.equalTo(micTopPadding)
make.width.equalTo(UIScreen.main.bounds.width*0.0581)
make.height.equalTo(UIScreen.main.bounds.height*0.045)
}
}
}
class ChatroomViewController: UIViewController{
weak var chatKeyboard:ChatKeyboard?
override func viewDidLoad() {
super.viewDidLoad()
chatKeyboard = ChatKeyboard(frame: CGRect(x: 0, y: 0, width: 300, height: 44))
}
}
I set breakpoint at "chatKeyboard = ChatKeyboard(frame: CGRect(x: 0, y: 0, width: 300, height: 44))"and then my log print:
===) ChatKeyboard init.
===) ChatKeyboard deinit.
A weak variable will be destroyed as soon as there are no strong references to it.
If you're creating a view and assigning it directly to a weak variable, it will be destroyed immediately. Weak IBOutlets work because they are added to a superview (creating a strong reference) before they are assigned to the variable. You can achieve this by using a local variable before assigning to your property:
let keyboard = ChatKeyboard(...)
view.addSubview(keyboard)
chatKeyboard = keyboard
However, there is no harm in your view controller having strong references to views it cares about, as long as those views don't also have strong references back to the view controller. They'll get destroyed when the view controller is destroyed.
Reason is you have declared your chatKeyBoard as
weak var chatKeyboard:ChatKeyboard?
Which means your viewController will not hold any strong reference to the view you load. Hence reference count of the view will not vary.
In your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
chatKeyboard = ChatKeyboard(frame: CGRect(x: 0, y: 0, width: 300, height: 44))
}
You instantiate the view but because view was weakly held as soon as the control goes out of the scope of viewDidLoad ARC released the view.
If you want your view to be accessible even after viewDidLoad called declare a strong a refrence
var chatKeyboard:ChatKeyboard?
Hope it helps
In a class or structure, do not use keyword "weak" to describe the property which you will init in the same class or structure.
I'm making app and I'm trying to make a View which contains a Label with a question. I want this view in my app and because I will use it repeatedly, I made a class (If I want to make some change, I can do It from one place). The UIView is called questionView (var questionView = UIView()). Problem is when I want to make questionView a subview of view. The error says that I don't have have "view" which I understand. I don't have view but how can I get it? Thank you
This is what is inside my Question class:
import Foundation
import UIKit
class Question {
// PROPERTIES:
var questionLabel = UILabel()
var questionView = UIView()
// METHODS:
func createQuestion (input:String) {
// some code .... not important
// THIS:
self.view.addSubview(questionView)
}
// ... next code, also not important
}
UPDATE:
There is my solution. It works BUT I think that it's not correct from a programming standpoint. Can anybody tell me anything about it? Thank you
My class in separate swift file:
My class in separate swift file:
class LabelClass {
var view = UIView()
init (view: UIView) {
self.view = view
}
var lbl = UILabel()
var lblView = UIView()
func makeLabel () {
self.lbl.frame = CGRectMake(0, 0, 150, 50)
self.lbl.text = "Text text text"
self.lbl.numberOfLines = 0
self.lblView.frame = CGRectMake(20, 20, 150, 50)
self.lblView.addSubview(self.lbl)
self.view.addSubview(lblView)
}
}
Piece of code my ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Added code:
var object = LabelClass(view: self.view)
object.makeLabel()
}
I don't know Swift, but as far as I know, only instances of UIViewController have a view property, the class Question does not, so you cannot add subviews to it.
What you probably want is making a subclass of UIView which contains a question label, or to add the questionLabel as a subview of questionView.
It is because you are trying to add your view to a normal Swift class which doesn't have a self.view instance. Your Question class must be a subclass of UIViewController cocoa class that it has a self.view instance and override methods.
class Question:UIViewController {
// PROPHERITIES:
var questionLabel = UILabel()
var questionView = UIView()
// METHODS:
override func viewDidLoad() {
createQuestion("foo")
}
func createQuestion (input:String) {
// some code .... not important
// THIS:
self.view.addSubview(questionView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// ... next code, also not important
}