Store window size for sheet view controller - swift

Is there any way to store window size in user defaults for view controller showed as a sheet? I'm using storyboard and segue with type sheet.

Result of my research
class SheetController: NSViewController, NSWindowDelegate {
// MARK: - Interface Builder
#IBInspectable var autosave: String?
// MARK: - NSViewController
override func viewWillAppear() {
super.viewWillAppear()
guard let window = self.view.window, let saveName = autosave else {
return
}
window.delegate = self
// restore window size
window.setFrameUsingName(saveName)
}
// MARK: - NSWindowDelegate
func windowDidEndLiveResize(notification: NSNotification) {
guard let saveName = autosave else {
return
}
// save new window size
self.view.window?.saveFrameUsingName(saveName)
}
}

Related

How to programmatically save the content written in TextView to a text file when terminating the app?

I want to save the content of the text view when the user closes the app.
I used the following codes to do so, but I cannot get the up-to-date string of the textview when closing the app. So, the produced text file is blank.
How should I access to the NSTextView from AppDelegate to save its content?
ViewController.swift
import Cocoa
class ViewController: NSViewController {
static var textViewString: String = ""
#IBOutlet var textView: NSTextView!{
didSet{
ViewController.textViewString = textView.string
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// start with hidden and show after moving to the main screen
DispatchQueue.main.async {
//keep the window top
self.view.window?.level = .floating
//set up the main display as the display where window shows up
let screens = NSScreen.screens
var pos = NSPoint()
pos.x = screens[0].visibleFrame.midX
pos.y = screens[0].visibleFrame.midY
self.view.window?.setFrameOrigin(pos)
self.view.window?.zoom(self)
self.view.window?.level = .floating
//self.view.window?.backgroundColor = NSColor.white
//stop the user from moving window
self.view.window?.isMovable = false
//disable resizable mode
self.view.window?.styleMask.remove(.resizable)
self.view.window?.setIsVisible(true)
}
//set up font for the reflectionForm
textView.font = NSFont.systemFont(ofSize: 30)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func saveTextViewString(){
if let documentDirectoryFileURL = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
let fileName = "savedText.txt"
let targetTextFilePath = documentDirectoryFileURL + "/" + fileName
do {
try ViewController.textViewString.write(toFile: targetTextFilePath, atomically: true, encoding: String.Encoding.utf8)
print("successfully recorded: \(ViewController.textViewString.description) at \(fileName.utf8CString)")
} catch let error as NSError {
print("failed to write: \(error)")
}
}
}
}
AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//save the string in the textview into a text file
ViewController().saveTextViewString()
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
Thank you for the #jnpdx's comments, I was able to solve this by just declaring ViewController in the AppDelegate by stating var viewController: ViewController!
ViewController.swift
import Cocoa
class ViewController: NSViewController {
#IBOutlet var textView: NSTextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// start with hidden and show after moving to the main screen
DispatchQueue.main.async {
//keep the window top
self.view.window?.level = .floating
//set up the main display as the display where window shows up
let screens = NSScreen.screens
var pos = NSPoint()
pos.x = screens[0].visibleFrame.midX
pos.y = screens[0].visibleFrame.midY
self.view.window?.setFrameOrigin(pos)
self.view.window?.zoom(self)
self.view.window?.level = .floating
//self.view.window?.backgroundColor = NSColor.white
//stop the user from moving window
self.view.window?.isMovable = false
//disable resizable mode
self.view.window?.styleMask.remove(.resizable)
self.view.window?.setIsVisible(true)
}
//set up font for the reflectionForm
textView.font = NSFont.systemFont(ofSize: 30)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func saveTextViewString(){
if let documentDirectoryFileURL = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
let fileName = "savedText.txt"
let targetTextFilePath = documentDirectoryFileURL + "/" + fileName
do {
try textView.string.write(toFile: targetTextFilePath, atomically: true, encoding: String.Encoding.utf8)
print("successfully recorded: \(textView.string.description) at \(fileName.utf8CString)")
} catch let error as NSError {
print("failed to write: \(error)")
}
}
}
}
AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
//connect viewController with ViewController
var viewController: ViewController!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//save the string in the textview into a text file
viewController.saveTextViewString()
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

NScrollView in NSPopover autolayout issue

I have an NSPopover which contains an NSViewController with a containing NSScrollView.
The Popover height has to be either the height of the NSScrollView content or the current window. Once it hits the bounds of the window it should scroll.
Using Snapkit
I have added the NSScrollView to the controller:
view.addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
make.edges.equalTo(view)
make.height.equalTo(mainView.content.snp.height)
}
This works fine until the content is greater than the window, then what happens is the NSScrollView will not scroll to the top of the content because the view has pushed itself upwards out of bounds.
I have gone down the route of removing the height constraint and in the viewDidLayout try to update the height but it doesn't work.
If more code examples are needed let me know.
Finally got to the bottom of the issue and found a sensible solution.
The app I am developing has a few popovers that are required at various stages, to ensure that they closed as required I created a service that manages every popover, here is an example:
class PopoverService: NSObject {
enum PopoverType {
case subscription, edit
}
//================================================================================
// MARK: - Properties
//================================================================================
private var dismissingPopover = false
private lazy var currentPopover: NSPopover = {
let popover = NSPopover()
popover.delegate = self
return popover
}()
private var nextPopoverType: PopoverType?
private var currentView: NSView!
public static var delegate: PopoverServiceDelegate?
//================================================================================
// MARK: - Singleton
//================================================================================
static let shared = PopoverService()
//================================================================================
// MARK: - Helpers
//================================================================================
public static func increaseHeight(_ height: CGFloat) {
shared.currentPopover.contentSize.height = height
}
public static func isDisplayingType(_ type: PopoverType) -> Bool {
switch type {
case .edit:
return shared.currentPopover.contentViewController is EditEntryController
case .language:
return shared.currentPopover.contentViewController is CodeTypeController
default:
return false
}
}
public static func displayPopover(type: PopoverType, fromView view: NSView) {
shared.nextPopoverType = type
shared.currentView = view
switch type {
case .subscription:
displaySubscriptionPopoverFrom(view)
// Create functions to display your popovers
}
}
static func dismissPopover(clearUpcoming: Bool = true) {
if clearUpcoming {
shared.nextPopoverType = nil
}
shared.currentPopover.performClose(nil)
if shared.currentPopover.contentViewController == nil {
shared.dismissingPopover = false; return
}
}
}
extension PopoverService: NSPopoverDelegate {
func popoverDidClose(_ notification: Notification) {
currentPopover.contentViewController = nil
dismissingPopover = false
guard let nextPopoverType = nextPopoverType else { return }
PopoverService.displayPopover(
type: nextPopoverType,
fromView: currentView,
entry: currentEntry
)
}
}
To update the current popover, there is a function increaseHeight which takes and CGFloat and will update the current popovers height.
In the NSViewController override the viewDidLayout():
override func viewDidLayout() {
super.viewDidLayout()
let windowFrameHeight = view.window?.frame.size.height ?? 0
let contentHeight = scrollView.content.frame.height
let adjustment = contentHeight > windowFrameHeight ? windowFrameHeight : contentHeight
PopoverService.increaseHeight(adjustment)
if contentHeight > 0 && firstLayout {
if let documentView = scrollView.documentView {
documentView.scroll(NSPoint(x: 0, y: documentView.bounds.size.height))
}
}
}
The scrollView will need to be forced to the top so there is a variable firstLayout which you can set to true in the viewDidAppear

How to update variable in MVVM?

I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪

Swift function textfield got focus OSX

Currently I am having multiple textfields in a view. If the user taps at one of them there should be a function responding to the event. Is there a way on how to do react (if a textfield got the focus)? I tried it with the NSTextFieldDelegate method but there is no appropriate function for this event.
This is how my code looks at the moment:
class ViewController: NSViewController, NSTextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let textField = NSTextField(frame: CGRectMake(10, 10, 37, 17))
textField.stringValue = "Label"
textField.bordered = false
textField.backgroundColor = NSColor.controlColor()
view.addSubview(textField)
textField.delegate = self
let textField2 = NSTextField(frame: CGRectMake(30, 30, 37, 17))
textField2.stringValue = "Label"
textField2.bordered = false
textField2.backgroundColor = NSColor.controlColor()
view.addSubview(textField2)
textField2.delegate = self
}
func control(control: NSControl, textShouldBeginEditing fieldEditor: NSText) -> Bool {
print("working") // this only works if the user enters a charakter
return true
}
}
The textShouldBeginEditing function only handles the event if the user tries to enter a character but this isn't what I want. It has to handle the event if he clicks on the textfield.
Any ideas, thanks a lot?
Edit
func myAction(sender: NSView)
{
print("aktuell: \(sender)")
currentObject = sender
}
This is the function I want to call.
1) Create a subclass of NSTextField.
import Cocoa
class MyTextField: NSTextField {
override func mouseDown(theEvent:NSEvent) {
let viewController:ViewController = ViewController()
viewController.textFieldClicked()
}
}
2) With Interface building, select the text field you want to have a focus on. Navigate to Custom Class on the right pane. Then set the class of the text field to the one you have just created.
3) The following is an example for ViewController.
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
4) Adding text fields programmatically...
import Cocoa
class ViewController: NSViewController {
let myField:MyTextField = MyTextField()
override func viewDidLoad() {
super.viewDidLoad()
//let myField:MyTextField = MyTextField()
myField.setFrameOrigin(NSMakePoint(20,70))
myField.setFrameSize(NSMakeSize(120,22))
let textField:NSTextField = NSTextField()
textField.setFrameOrigin(NSMakePoint(20,40))
textField.setFrameSize(NSMakeSize(120,22))
self.view.addSubview(myField)
self.view.addSubview(textField)
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
I know it’s been answered some while ago but I did eventually find this solution for macOS in Swift 3 (it doesn’t work for Swift 4 unfortunately) which notifies when a textfield is clicked inside (and for each key stroke).
Add this delegate to your class:-
NSTextFieldDelegate
In viewDidLoad() add these:-
imputTextField.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: Notification.Name.NSTextViewDidChangeSelection, object: nil)
Then add this function:-
func textDidChange(_ notification: Notification) {
print("Its come here textDidChange")
guard (notification.object as? NSTextView) != nil else { return }
let numberOfCharatersInTextfield: Int = textFieldCell.accessibilityNumberOfCharacters()
print("numberOfCharatersInTextfield = \(numberOfCharatersInTextfield)")
}
Hope this helps others.

Swift Delegate setting a label from a custom popUp textfield

I have a custom popup view that has a UIDatePicker. This, when changed, changes the date of the save time. I also want the label on the in the CustomCell to be updated if the date has changed. I have used a delegate protocol to update the table but I cannot get this protocol to transfer the information on save. Can you help? I think I have hooked up all the correct code in the viewController class. I have tried this answer but I cannot set the delegate in the target class and there isn't a segue A Swift example of Custom Views for Data Input (custom in-app keyboard)
protocol DatePopUpViewDelegate: class {
func pastDate(date: String) // date that is chosen in picker
func isPastDateSet(isSet: Bool) // has chosen new date
}
#IBDesignable class DatePopUpView: UIView {
var delegate: DatePopUpViewDelegate?
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "DatePopUp", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
delegate?.isPastDateSet(false)
return view
}
// close popup
#IBAction func closeButtonDatePopUp(sender: AnyObject) {
if dateToSave != openTime {
if let dateToSave = dateToSave {
SaveData.changedSaveTime = dateToSave
delegate?.pastDate(dateToSave)
delegate?.isPastDateSet(true)
}
} else {
SaveData.changedSaveTime = ""
delegate?.isPastDateSet(false)
}
}
class SaveTableViewCell: UITableViewCell, DatePopUpViewDelegate {
var changeDateLabel: Bool = false
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
changeDateLabel = false
datePopUpViewControllert.delegate = self
}
// delegate functions
func pastDate(date: String) {
self.labelDate = date
print("del date \(date)")
}
func isPastDateSet(isSet: Bool) {
self.changeDateLabel = isSet
print("is set by delegate \(isSet)")
}