Inject ViewModifier to SwiftUI View - swift

Current Situtation
I work on an iOS SDK containing several UIViewControllers that can be customized through a delegate function passing a String as the identifier and getting a customized CALayer. With that, the host application can customize how the SDK looks.
This is an example:
extension ViewController: NINChatSessionDelegate {
func ninchat(_ session: NINChatSession, overrideLayer assetKey: CALayerConstant) -> CALayer? {
switch assetKey {
case .ninchatPrimaryButton:
let layer = CALayer()
layer.backgroundColor = UIColor.gray.cgColor
layer.masksToBounds = true
layer.cornerRadius = 20.0
return layer
default:
return nil
}
}
Problem
Nowadays, I'm trying to migrate the code to SwiftUI and Combine. But I cannot find a way to offer host applications to inject their own attributes. As far as I have understood, a ViewModifier struct is used to apply custom attributes to one or multiple View(s) in SwiftUI. However, I could not yet find a way to inject them to the SDK because a ViewModifier protocol has Self or associated type requirements
I appreciate any helps and guides.

Related

NSTextView not being found in project scope SwiftUI

I'm trying to add a TextEditor to my project with a different background color. I'd like to just use the simple extension that's found in this post: Transparent Background for TextEditor in SwiftUI
The problem is this--when I paste it in, I get the following errors as seen below:
extension NSTextView { // Cannot find type 'NSTextView' in scope.
open override var frame: CGRect {
didSet {
backgroundColor = .clear // Cannot find 'backgroundColor' in scope.
drawsBackground = true // Cannot find 'drawsBackground' in scope.
}
}
}
I assumed NSTextView was a built-in Swift type because there's documentation here (https://developer.apple.com/documentation/appkit/nstextview). I'm importing SwiftUI and Foundation at the top of the file. I'm trying to compile on an iPhone target set to iOS 14.4 so this shouldd definitely be working, right? What am I missing?

Set Accessibility Identifier in UIView Extension

I worked on Mobile Test Automation.Previous, some elements don't have any identifier but i need to import identifiers for testing issues.
So I decide to write an extension to UIView, hereby that code will be affect all codes so I wont need to add one by one.
How can I do ? Should I write on init or awakeFromNib ?
Thanks in advance.
Generally you'll want to have specific accessibility identifiers for elements you want to expose to the accessibility system.
You can set those directly in Storyboards/Interface Builder, or you can set them in your view's initializer when implementing UIs programatically:
class MyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.accessibilityIdentifier = "my-custom-view"
let label = UILabel()
label.accessibilityIdentifier = "my-custom-label"
self.addSubview(label)
}
}

Making ViewController slimmer by moving TableView away

I am quite new into programming and facing some issues while trying to slim down my ViewController by moving creation of the tableView and associated views to the separate class and moving delegates and datasource from VC to separate one.
My constellation of current files and work is as follows:
After network connection, when data are received, I am calling inside a closure class to set view which will have embedded UITableView.
Fetched data are being saved into the CoreData stack
I instantiate view from another class
var detailView: DetailView! { return self.view as? DetailView }
Once I will download first part of the UI (separate call which works fine)
I am moving onto the part which is messy and surpass my abilities
I call a function createReposCard()
dispatchGroup.notify(queue: dispatchQueue) {
DispatchQueue.main.async {
// slide animation
UIView.animate(withDuration: 0.75, delay: 0.5, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [], animations: {
self.detailView.reposCard.center = CGPoint(x: self.detailView.reposCard.center.x, y: self.detailView.reposCard.center.y-UIScreen.main.bounds.height)
}, completion: nil)
self.detailView.createReposCard(for: self.repos)
self.detailView.detailsTableView.reloadData()
self.activityIndicator.stopAnimating()
self.activityIndicator.removeFromSuperview()
}
}
Code is incomplete to depict the problem only, but what it does one by one.
I am passing info about selected user (irrelevant info)
then I am making a reference to my DetailsViewController which still e.g. holds CoreData stack
In initializer I am instantiating detailsViewController and passing it onto the class which holds delegates (I am passing it to have there reference to the CoreData)
class DetailView: UIView {
var selectedUser: User?
var detailsViewController: DetailsViewController!
let detailsTableView: UITableView = {
let tableView = UITableView()
tableView.frame = CGRect.zero
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reposCell")
return tableView
}()
init(selectedUser:User, frame: CGRect) {
super.init(frame: frame)
self.selectedUser = selectedUser
detailsViewController = DetailsViewController()
let tableViewDelegates = TableViewDelegates(detailsViewController: detailsViewController)
detailsTableView.delegate = tableViewDelegates
detailsTableView.dataSource = tableViewDelegates
}
And finally code jumps into the depicted class where I am knocked down by "Unexpectedly found nil while implicitly unwrapping..."
public class TableViewDelegates: NSObject, UITableViewDataSource,UITableViewDelegate {
private let detailsViewController: DetailsViewController
init(detailsViewController: DetailsViewController){
self.detailsViewController = detailsViewController
super.init()
}
public func numberOfSections(in tableView: UITableView) -> Int {
return detailsViewController.fetchedResultsController.sections?.count ?? 1
}
...
Actually I don't know if my concept is good as I feel quite lost, but my intentions were as follows:
Move View creation to other class
Move TableView delegates to other class
Finally - move networking and CoreData to other class.
But as I see, simple data passing is overcoming my abilities.
I think this question can be divided into two parts:
1) Why is my variable nil when I unwrap it?
I don't think we have enough information to answer this accurately, but my overall approach would be like this:
Check what nil variable is being unwrapped;
Make sure this variable is being properly initialized;
Make sure that the object is not being incorrectly deinitialized;
If you're using Storyboard, use the inspectors to check if everything is set correctly.
There's a particular observation about step 2: you should check the order of execution of your methods to make sure that the variable is properly initialized. Why am I emphasizing this? Because there's a chance that some view (e.g., detailView) is initialized like an ordinary UIView, and then you try to access an element that is not part of a UIView object (e.g., a table view). In other words, check if you're setting the custom views before you try to access them.
2) How to structure the project in a more organized way?
This is a more interesting question, and I think that choosing a better approach will help you to avoid issues like what you're experiencing. I will divide this into some topics. Everything here is my personal opinion and doesn't necessarily reflect the best approach, especially because "best" is subjective here.
PersistenceManager class
First, passing a reference of a view controller to another class just to access CoreData doesn't seem like a good option. A better approach would be to have a PersistenceManager class, for example. You could use an object of this class to fetch and save data. You could pass this object instead of the view controller.
In some architectures (e.g., VIPER), it wouldn't be correct for the view controller to access the persistence directly, so it would be more appropriate to pass an array of already fetched objects. For example:
class TableViewController {
private let tableView: UITableView!
private var currentlyDisplayedUsers: [Users]?
func displayUsers(_ users: [Users]) {
self.currentlyDisplayedUsers = users
self.tableView.reloadData()
}
}
In the example above, the tableView would display currentlyDisplayedUsers, which would be updated by the method displayUsers, which would be called by someone else, like a Presenter.
Network class
Second, I think you should have a network class to download data from the internet. You would use instances of the Network class in the application logic. In other words, you would have something like
// This is in the application logic
// In MVC, the logic is in the Controller, while in VIPER the logic is in the Interactor
class ApplicationLogic {
let networkAPI: Network?
...
func fetchUserData() {
networkAPI?.fetchUsers() { data, error in
// Update the UI based on the response
// Using the previous example, you could call displayUsers here
}
}
}
TableView, TableViewDelegate, and TableViewDataSource
Finally, how to organize these guys. UITableViewDelegate is responsible for telling us about events in the table, while UITableViewDataSource is responsible for filling the table with data, which means that both are strongly related to the table view itself. That said, imho, both should be implemented in different swift files, but as extensions of the view controller that has a reference to the table view. Something like
// TableViewController+UITableViewDelegate.swift
extension TableViewController: UITableViewDelegate {
...
}
// TableViewController+UITableViewDataSource.swift
extension TableViewController: UITableViewDataSource {
...
}
Using this approach, the delegate and the data source would have access to the users array, mentioned earlier. Here is an example of how to implement a similar approach using VIPER.
So, I hope I could provide a basic idea on these topics. If you want to understand more about how to structure your code, I suggest researching iOS design patterns and architectural patterns. Some architectural design patterns that are famous in iOS development are MVC, MVP, MVVM, and VIPER.

Swift: slow build times when changing struct code

tl;dr
Applying a code change to a commonly used struct causes very slow build times. Can this be avoided?
I have a rather large Swift project (Xcode 9.2) where I use a struct to hold all styling information (colors, spacings, etc.) used by in the app, something like:
struct Style {
var iconColor: UIColor = .darkGray
var lightTextColor: UIColor = .gray
// ... and many more properties ...
static var defaultStyle: Style {
return Style()
}
static var fancyStyle: Style {
var style = Style()
// ... override style props for more fancyness ...
return style
}
}
Every view controller (there are about 30 VCs in the project, all created in code -- no storyboards used) has a Style instance and uses it when rendering its UI:
class MyViewController: UIViewController {
var style = Style.defaultStyle // can be overridden by creator of the VC
override func viewDidLoad() {
super.viewDidLoad()
myLabel.textColor = style.lightTextColor
}
}
This works pretty nicely and allows to change settings in one place only, without polluting the namespace with global constants.
However, I noticed that when changing a property's default value inside the struct the compile time rises dramatically, Xcode basically performs a full rebuild. E.g. changing the above definition of Style.lightTextColor to var lightTextColor: UIColor = .green leads to a build time comparable to a full rebuild of the project. If, however, I change the value directly where it is used; e.g. in the view controller: myLabel.textColor = .green, the project builds very quickly.
Is there a solution for this, preferably by configuring the compiler to do ... less work in this case?

Is IBDesignable broken for AppKit in Xcode 9.2 and Swift 4?

Most questions, and answers related to this, are based on older versions of both Xcode and Swift. Additionally, 90 percent of the questions relate to UIKit and drawing custom controls.
I am adding a standard button, that is centered inside a custom control, decorated with IBDesignable.
import Cocoa
#IBDesignable public class ButtonPresetView: NSView {
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
initialControlSetup()
}
public required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
initialControlSetup()
}
private func initialControlSetup() {
let button = NSButton(title: "Hello", target: nil, action: nil)
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
// Configure button
centerXAnchor.constraint(equalTo: button.centerXAnchor).isActive = true
centerYAnchor.constraint(equalTo: button.centerYAnchor).isActive = true
}
}
I add a custom view to the application and set the class property in the Identity Inspector to my custom class (ButtonPresetView).
It should show the button centered on the canvas, but the canvas is blank.
Not sure many people use it this way, but it worked gloriously with Swift 3 in Xcode 8.3.
Does anyone else have this problem?
I was able to get this to work in the latest Xcode by adding the following two lines to the top of the initialControlSetup function:
wantsLayer = true
canDrawSubviewsIntoLayer = true
I think this basically tells the NSView to render in a way that is more similar to how iOS works. If this worked in Xcode 8.3 as you say, it's possible that Apple introduced this regression in Xcode 9 without realizing it.
Dave's answer is correct, I just want to make a note on the consequences of this solution.
When canDrawSubviewsIntoLayer is set to true, all its sub views, that did not enable wantsLayer specifically, will render its contents using the layer of the parent view with canDrawSubviewsIntoLayer set to true.
This means sub view animations is disabled, since they lack a backing layer of their own. To prevent this from happening during runtime, you can put canDrawSubviewsIntoLayer = true into the prepareForInterfaceBuilder() function.
On a curious note, Interface Builder does not render the control, if you explicitly set button.wantsLayer = true, which according to the "canDrawSubviewsIntoLayer" documentation, should give the control its own backing layer and not render itself into the parent layer.
This is purely speculation, but I'm guessing as an optimisation, Interface Builder only renders the top layers/controls of the content view.