I made an NSObject class that is receiving a couple of variables from a UIView class. I am successfully receiving the variables in the NSObject class and am processing them in my method receiveDims, where I append an array of Doubles. When I want to access this array in a second method (serialize), they show up as nil. Some of the research I have done on this suggest that this is a timing issue.
Does anybody have an idea as to why this is showing up as nil and not updating in serialize()? Is there a better way to have these arrays update in serialize()? Pertinent code is below.
class PostPath: NSObject {
var points:Array<PostPoint>
var color:UIColor
var width = [Double]()
var height = [Double]()
init(point:CGPoint, color:UIColor) {
self.color = color
self.points = Array<PostPoint>()
let newPoint = PostPoint(point: point)
points.append(newPoint)
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PostPath.receiveDims(_:)), name: "screenWidth", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PostPath.receiveDims(_:)), name: "screenHeight", object: nil)
}
func addPoint(point:CGPoint){
let newPoint = PostPoint(point: point)
points.append(newPoint)
}
func receiveDims(sender: NSNotification){
if sender.userInfo!["width"] != nil{
self.width.append(sender.userInfo!["width"] as! Double)
print("post path width \(self.width[0])")
}
if sender.userInfo!["height"] != nil{
self.height.append(sender.userInfo!["height"] as! Double)
print("post path height \(self.height[0])")
}
}
func serialize() -> NSDictionary{
print(self.width[0])
let dictionary = NSMutableDictionary()
let cgColor = color.CGColor
dictionary["color"] = CIColor(CGColor: cgColor).stringRepresentation
let pointsOfPath = NSMutableArray()
for point in points{
let pointDictionary = NSMutableDictionary()
pointDictionary["x"] = point.x!
pointDictionary["y"] = point.y!
pointsOfPath.addObject(pointDictionary)
}
dictionary["points"] = pointsOfPath
return dictionary
}
}
The class DrawingView is where the width and height variables originate. They are stored to NSNotificationCenter and PostPath.serialize() are called in the following manner (I have trimmed the code to only show these parts):
class DrawingView: UIView {
func getDims(){
self.width = Double(self.frame.size.width)
self.height = Double(self.frame.size.height)
NSNotificationCenter.defaultCenter().postNotificationName("screenWidth", object: nil, userInfo: ["width": width])
NSNotificationCenter.defaultCenter().postNotificationName("screenHeight", object: nil, userInfo: ["height": height])
}
func resetPatch(sendToFirebase:Bool){
currentPointPath?.serialize()
}
func addPathToSend(path: PostPath)->String{
firebaseKey.setValue(path.serialize()) { (error:NSError!, ref:Firebase!) -> Void in
if let error = error{
print("Error saving path to firebase\(error)")
} else{
pathsInLine.removeObject(firebaseKey)
}
}
return firebaseKey.key
}
}
Related
How to load NSView from Xib properly?
My code:
var topLevelArray: NSArray? = nil
let outputValue = AutoreleasingUnsafeMutablePointer<NSArray>(&topLevelArray)
if Bundle.main.loadNibNamed("RadioPlayerView", owner: nil, topLevelObjects: outputValue) {
let views = outputValue.pointee
return views.firstObject as! RadioPlayerView
}
topLevelArray = nil
return nil
The problem is "outputValue" is a auto-release pointer, and as soon as I return from the function, the program crash with ACCESS_BAD_ADDRESS
I made an protocol and extension to do this:
import Cocoa
protocol NibLoadable {
static var nibName: String? { get }
static func createFromNib(in bundle: Bundle) -> Self?
}
extension NibLoadable where Self: NSView {
static var nibName: String? {
return String(describing: Self.self)
}
static func createFromNib(in bundle: Bundle = Bundle.main) -> Self? {
guard let nibName = nibName else { return nil }
var topLevelArray: NSArray? = nil
bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self }
return views.last as? Self
}
}
Usage:
final class MyView: NSView, NibLoadable {
// ...
}
// create your xib called MyView.xib
// ... somewhere else:
let myView: MyView? = MyView.createFromNib()
I solved this problem with a slightly different approach. Code in Swift 5.
If you want to create NSView loaded from .xib to e.g. addSubview and constraints from code, here is example:
public static func instantiateView<View: NSView>(for type: View.Type = View.self) -> View {
let bundle = Bundle(for: type)
let nibName = String(describing: type)
guard bundle.path(forResource: nibName, ofType: "nib") != nil else {
return View(frame: .zero)
}
var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(nibName), owner: nil, topLevelObjects: &topLevelArray)
guard let results = topLevelArray as? [Any],
let foundedView = results.last(where: {$0 is Self}),
let view = foundedView as? View else {
fatalError("NIB with name \"\(nibName)\" does not exist.")
}
return view
}
public func instantiateView() -> NSView {
guard subviews.isEmpty else {
return self
}
let loadedView = NSView.instantiateView(for: type(of: self))
loadedView.frame = frame
loadedView.autoresizingMask = autoresizingMask
loadedView.translatesAutoresizingMaskIntoConstraints = translatesAutoresizingMaskIntoConstraints
loadedView.addConstraints(constraints.compactMap { ctr -> NSLayoutConstraint? in
guard let srcFirstItem = ctr.firstItem as? NSView else {
return nil
}
let dstFirstItem = srcFirstItem == self ? loadedView : srcFirstItem
let srcSecondItem = ctr.secondItem as? NSView
let dstSecondItem = srcSecondItem == self ? loadedView : srcSecondItem
return NSLayoutConstraint(item: dstFirstItem,
attribute: ctr.firstAttribute,
relatedBy: ctr.relation,
toItem: dstSecondItem,
attribute: ctr.secondAttribute,
multiplier: ctr.multiplier,
constant: ctr.constant)
})
return loadedView
}
If there is no .xib file with the same name as the class name, then code will create class from code only. Very good solution (IMO) if someone wants to create the view from code and xib files in the same way, and keeps your code organized.
.xib file name and class name must have the same name:
In .xib file you should only have one view object, and this object has to have set class:
All you need to add in class code is instantiateView() in awakeAfter e.g.:
import Cocoa
internal class ExampleView: NSView {
internal override func awakeAfter(using coder: NSCoder) -> Any? {
return instantiateView() // You need to add this line to load view
}
internal override func awakeFromNib() {
super.awakeFromNib()
initialization()
}
}
extension ExampleView {
private func initialization() {
// Preapre view after view did load (all IBOutlets are connected)
}
}
To instantiate this view in e.g. ViewController you can create view like that:
let exampleView: ExampleView = .instantiateView() or
let exampleView: ExampleView = ExampleView.instantiateView()
but Swift have problems sometimes with instantiate like that:
let exampleView = ExampleView.instantiateView()
in viewDidLoad() in your controller you can add this view as subview:
internal override func viewDidLoad() {
super.viewDidLoad()
exampleView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(exampleView)
NSLayoutConstraint.activate(
[exampleView.topAnchor.constraint(equalTo: view.topAnchor),
exampleView.leftAnchor.constraint(equalTo: view.leftAnchor),
exampleView.rightAnchor.constraint(equalTo: view.rightAnchor),
exampleView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]
)
}
I am building a simple currency converter app. When ViewController gets opened it calls a function from CoinManager.swift:
class ViewController: UIViewController {
var coinManager = CoinManager()
override func viewDidLoad() {
super.viewDidLoad()
coinManager.delegate = self
coinManager.getCoinPrice(for: "AUD", "AZN", firstCall: true)
}
...
}
CoinManager.swift:
protocol CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String)
func tellTableView(descriptions: [String], symbols: [String])
func didFailWithError(error: Error)
}
struct CoinManager {
var delegate: CoinManagerDelegate?
let baseURL = "https://www.cbr-xml-daily.ru/daily_json.js"
func getCoinPrice (for currency1: String,_ currency2: String, firstCall: Bool) {
if let url = URL(string: baseURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}
if let safeData = data {
if let coinData = self.parseJSON(safeData) {
if firstCall {
var descriptions = [""]
let listOfCoins = Array(coinData.keys)
for key in listOfCoins {
descriptions.append(coinData[key]!.Name)
}
descriptions.removeFirst()
self.delegate?.tellTableView(descriptions: descriptions, symbols: listOfCoins)
}
if let coinInfo1 = coinData[currency1] {
let value1 = coinInfo1.Value
if let coinInfo2 = coinData[currency2] {
let value2 = coinInfo2.Value
//this line does not do anything the second time I call getCoinPrice:
self.delegate?.didUpdatePrice(price1: value1, currency1: currency1, price2: value2, currency2: currency2)
//And this one does work
print("delegate:\(currency1)")
} else {
print("no name matches currency2")
}
} else {
print("no name matches currency1")
}
}
}
}
task.resume()
}
}
func ParseJSON....
}
The method it calls (ViewController.swift):
extension ViewController: CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String) {
print("didUpdatePrice called")
DispatchQueue.main.async {
let price1AsString = String(price1)
let price2AsString = String(price2)
self.leftTextField.text = price1AsString
self.rightTextField.text = price2AsString
self.leftLabel.text = currency1
self.rightLabel.text = currency2
}
}
...
}
and finally, CurrencyViewController.swift:
var coinManager = CoinManager()
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
So when I launch the app i get following in my debug console:
didUpdatePrice called
delegate:AUD
And when I call getCoinPrice() from CurrencyViewController the delegate method does not get called. I know that my code goes through the delegate function line as I get this in debug console:
delegate:USD
I just can't wrap my head around it. The delegate method does not work when gets called second time. Even though it is called by the same algorithm
It's because you're creating a new object of CoinManager in CurrencyViewController where the delegate is not set. So you've to set the delegate every time you create a new instance of CoinManager.
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.delegate = self
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
Update: So, the above solution would require for you to make the delegate conformance in CurrencyViewController. If you're looking for an alternate solution you should probably pass the instance of coinManager in ViewController to CurrencyViewController. For that here are the things you need to update.
In CurrencyViewController:
class CurrencyViewController: UIViewController {
var coinManager: CoinManager! // you can optional unwrap if you intent to use CurrencyViewController without coinManager
//...
And in ViewController:
currencyViewController.coinManager = coinManager // passing the instance of coinManager
Can you share the full code of CoinManager? I see this part
if firstCall {
...
}
Maybe some block logic here or unhandled cases? And can you share the full code of protocol?
Also try to print something before this code:
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}
Life was fairly easy when I first test-developed a text-based application with NSDocument. Now, I have a lot more complicated document-based desktop application with several custom models other than a string with NSTextView. My subclass of NSDocument is the following.
import Cocoa
class Document: NSDocument {
// MARK: - Variables
var image = NSImage()
var myPasteModels = [PasteModel]()
var myPanModel: PanModel?
var myWinModel: WindowModel?
// MARK: - Initialization
override init() {
super.init()
}
// MARK: - Auto saving
override class var autosavesInPlace: Bool {
return false
}
override func data(ofType typeName: String) throws -> Data {
if let viewController = windowControllers[0].contentViewController as? MainViewController {
if viewController.imageModels.count > 0 {
viewController.saveSubViewPositions()
if let window = viewController.view.window {
var pasteModels = [PasteModel]()
for i in 0..<viewController.imageModels.count {
let imageModel = viewController.imageModels[i]
...
...
}
NSKeyedArchiver.setClassName("ColorModel", for: ColorModel.self)
NSKeyedArchiver.setClassName("TextModel", for: TextModel.self)
NSKeyedArchiver.setClassName("ShapeModel", for: ShapeModel.self)
NSKeyedArchiver.setClassName("ShadeModel", for: ShadeModel.self)
NSKeyedArchiver.setClassName("LineModel", for: LineModel.self)
NSKeyedArchiver.setClassName("GradientModel", for: GradientModel.self)
NSKeyedArchiver.setClassName("ArrowModel", for: ArrowModel.self)
NSKeyedArchiver.setClassName("PasteModel", for: PasteModel.self)
NSKeyedArchiver.setClassName("PanModel", for: PanModel.self)
NSKeyedArchiver.setClassName("WindowModel", for: WindowModel.self)
let panModel = PanModel(frameWidth: viewController.panView.frame.size.width, frameHeight: viewController.panView.frame.size.height)
let winModel = WindowModel(width: window.frame.width, height: window.frame.height)
let dict = ["PasteModel": pasteModels, "PanModel": panModel, "WindowModel": winModel] as [String : Any]
do {
let modelData = try NSKeyedArchiver.archivedData(withRootObject: dict, requiringSecureCoding: false)
return modelData
} catch let error as NSError {
Swift.print("\(error)")
}
}
}
}
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func save(withDelegate delegate: Any?, didSave didSaveSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) {
if let _ = fileURL {
Swift.print("Saved!!!")
} else {
Swift.print("Not saved yet...")
NSApp.sendAction(#selector(NSDocument.saveAs(_:)), to: nil, from: self)
}
}
override func prepareSavePanel(_ savePanel: NSSavePanel) -> Bool {
savePanel.allowedFileTypes = ["fss"]
savePanel.allowsOtherFileTypes = true
savePanel.isExtensionHidden = false
return true
}
}
The problem that I have is that the application never saves a document if I choose Save As under File (or press Command + Shift + S). If I choose Save As, the application goes beep and dismiss the command selection. It does enter the prepareSavePanel method if I set a break point there. So what can I do to go any further? Thanks.
How to load NSView from Xib properly?
My code:
var topLevelArray: NSArray? = nil
let outputValue = AutoreleasingUnsafeMutablePointer<NSArray>(&topLevelArray)
if Bundle.main.loadNibNamed("RadioPlayerView", owner: nil, topLevelObjects: outputValue) {
let views = outputValue.pointee
return views.firstObject as! RadioPlayerView
}
topLevelArray = nil
return nil
The problem is "outputValue" is a auto-release pointer, and as soon as I return from the function, the program crash with ACCESS_BAD_ADDRESS
I made an protocol and extension to do this:
import Cocoa
protocol NibLoadable {
static var nibName: String? { get }
static func createFromNib(in bundle: Bundle) -> Self?
}
extension NibLoadable where Self: NSView {
static var nibName: String? {
return String(describing: Self.self)
}
static func createFromNib(in bundle: Bundle = Bundle.main) -> Self? {
guard let nibName = nibName else { return nil }
var topLevelArray: NSArray? = nil
bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self }
return views.last as? Self
}
}
Usage:
final class MyView: NSView, NibLoadable {
// ...
}
// create your xib called MyView.xib
// ... somewhere else:
let myView: MyView? = MyView.createFromNib()
I solved this problem with a slightly different approach. Code in Swift 5.
If you want to create NSView loaded from .xib to e.g. addSubview and constraints from code, here is example:
public static func instantiateView<View: NSView>(for type: View.Type = View.self) -> View {
let bundle = Bundle(for: type)
let nibName = String(describing: type)
guard bundle.path(forResource: nibName, ofType: "nib") != nil else {
return View(frame: .zero)
}
var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(nibName), owner: nil, topLevelObjects: &topLevelArray)
guard let results = topLevelArray as? [Any],
let foundedView = results.last(where: {$0 is Self}),
let view = foundedView as? View else {
fatalError("NIB with name \"\(nibName)\" does not exist.")
}
return view
}
public func instantiateView() -> NSView {
guard subviews.isEmpty else {
return self
}
let loadedView = NSView.instantiateView(for: type(of: self))
loadedView.frame = frame
loadedView.autoresizingMask = autoresizingMask
loadedView.translatesAutoresizingMaskIntoConstraints = translatesAutoresizingMaskIntoConstraints
loadedView.addConstraints(constraints.compactMap { ctr -> NSLayoutConstraint? in
guard let srcFirstItem = ctr.firstItem as? NSView else {
return nil
}
let dstFirstItem = srcFirstItem == self ? loadedView : srcFirstItem
let srcSecondItem = ctr.secondItem as? NSView
let dstSecondItem = srcSecondItem == self ? loadedView : srcSecondItem
return NSLayoutConstraint(item: dstFirstItem,
attribute: ctr.firstAttribute,
relatedBy: ctr.relation,
toItem: dstSecondItem,
attribute: ctr.secondAttribute,
multiplier: ctr.multiplier,
constant: ctr.constant)
})
return loadedView
}
If there is no .xib file with the same name as the class name, then code will create class from code only. Very good solution (IMO) if someone wants to create the view from code and xib files in the same way, and keeps your code organized.
.xib file name and class name must have the same name:
In .xib file you should only have one view object, and this object has to have set class:
All you need to add in class code is instantiateView() in awakeAfter e.g.:
import Cocoa
internal class ExampleView: NSView {
internal override func awakeAfter(using coder: NSCoder) -> Any? {
return instantiateView() // You need to add this line to load view
}
internal override func awakeFromNib() {
super.awakeFromNib()
initialization()
}
}
extension ExampleView {
private func initialization() {
// Preapre view after view did load (all IBOutlets are connected)
}
}
To instantiate this view in e.g. ViewController you can create view like that:
let exampleView: ExampleView = .instantiateView() or
let exampleView: ExampleView = ExampleView.instantiateView()
but Swift have problems sometimes with instantiate like that:
let exampleView = ExampleView.instantiateView()
in viewDidLoad() in your controller you can add this view as subview:
internal override func viewDidLoad() {
super.viewDidLoad()
exampleView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(exampleView)
NSLayoutConstraint.activate(
[exampleView.topAnchor.constraint(equalTo: view.topAnchor),
exampleView.leftAnchor.constraint(equalTo: view.leftAnchor),
exampleView.rightAnchor.constraint(equalTo: view.rightAnchor),
exampleView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]
)
}
Is it possible to get the frame, actually its height, of the keyboard dynamically? As I have a UITextView and I would like to adjust its height according to the keyboard frame height, when the input method of the keyboard is changed. As you know, different input methods may have different keyboard frame height.
try this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification
object:nil];
- (void)keyboardWasShown:(NSNotification *)notification
{
// Get the size of the keyboard.
CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
//Given size may not account for screen rotation
int height = MIN(keyboardSize.height,keyboardSize.width);
int width = MAX(keyboardSize.height,keyboardSize.width);
//your other code here..........
}
Tutorial for more information
Just follow this tutorial from Apple and you will get what you want. Apple Documentation. In order to determine the area covered by keyboard please refer to this tutorial.
For the Swift 3 users, the #Hector code (with some additions) would be:
In your viewDidLoad add the observer :
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidShow(_:)), name: .UIKeyboardDidShow , object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidHide(_:)), name: .UIKeyboardDidHide , object: nil)
Then implement those methods:
func keyboardDidShow(_ notification: NSNotification) {
print("Keyboard will show!")
// print(notification.userInfo)
let keyboardSize:CGSize = (notification.userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue.size
print("Keyboard size: \(keyboardSize)")
let height = min(keyboardSize.height, keyboardSize.width)
let width = max(keyboardSize.height, keyboardSize.width)
}
func keyboardDidHide(_ notification: NSNotification) {
print("Keyboard will hide!")
}
You can add this code to the view which contains the text field in Swift 3. This will make the text field animate up and down with the keyboard.
private var keyboardIsVisible = false
private var keyboardHeight: CGFloat = 0.0
// MARK: Notifications
private func registerForKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
private func deregisterFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
// MARK: Triggered Functions
#objc private func keyboardWillShow(notification: NSNotification) {
keyboardIsVisible = true
guard let userInfo = notification.userInfo else {
return
}
if let keyboardHeight = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.height {
self.keyboardHeight = keyboardHeight
}
if !textField.isHidden {
if let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber,
let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
animateHUDWith(duration: duration.doubleValue,
curve: UIViewAnimationCurve(rawValue: curve.intValue) ?? UIViewAnimationCurve.easeInOut,
toLocation: calculateTextFieldCenter())
}
}
}
#objc private func keyboardWillBeHidden(notification: NSNotification) {
keyboardIsVisible = false
if !self.isHidden {
guard let userInfo = notification.userInfo else {
return
}
if let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber,
let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
animateHUDWith(duration: duration.doubleValue,
curve: UIViewAnimationCurve(rawValue: curve.intValue) ?? UIViewAnimationCurve.easeInOut,
toLocation: calculateTextFieldCenter())
}
}
}
// MARK: - Helpers
private func animateHUDWith(duration: Double, curve: UIViewAnimationCurve, toLocation location: CGPoint) {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(TimeInterval(duration))
UIView.setAnimationCurve(curve)
textField.center = location
UIView.commitAnimations()
}
private func calculateTextFieldCenter() -> CGPoint {
if !keyboardIsVisible {
return self.center
} else {
let yLocation = (self.view.frame.height - keyboardHeight) / 2
return CGPoint(x: self.center.x, y: yLocation)
}
}