Gieta Laksmana
  • Blog
  • Profile
  • Projects
    • VR World Arcade
    • Punch For Fun
    • Spectrum
    • Dimension Battle
  • Resume

XCode10 and prebuild script output files xcfilelist

10/14/2018

0 Comments

 
​Found another quirk with XCode 10. Although this time they did mention it on their release notes (I read the release notes after I hit this issue).

Let's say you have a simple project that generates a file on your pre-build phase, and you include that file on the compilation phase.
Picture
Picture
​This setup will work just fine on Xcode 9 with the old build system. However, on the new build system on Xcode 10, when you build the project for the first time you will get an error saying that the file couldn't be found:
Picture
​When you try to check if the file was there, you will be hit with confusion as the file is generated properly. So what's up with that?

According to Xcode 10 release notes, you now have to explicitly add the generated files name in the Output Files field of the phase, otherwise the build system might try to search for the files before they are generated.​ 
Picture
If you have several generated files that are being used by multiple targets and don't want to duplicate your efforts, you can use the new xcfilelist file that is available for XCode 10. Simply list the generated files inside the xcfilelist, and assign the xcfilelist as an Output File Lists of the phase
Picture
Picture
aand voila, the build system should now find your file correctly

Here's the full Xcode 10 release notes:
​
https://developer.apple.com/documentation/xcode_release_notes/xcode_10_release_notes/build_system_release_notes_for_xcode_10

0 Comments

CoreData on macOS Mojave (10.14.0) shenanigans

9/26/2018

0 Comments

 
UPDATE (10/16/2018): Apple has closed my bug report about this as resolved. So hopefully the fix will be out on Mojave 10.14.1

tldr: overwriting a binary blob greater than 100kb will unnecessarily create extra files on each save with CoreData on macOS Mojave (10.14.0)
​Hookay, I was pretty excited to try out macOS Mojave and their dark mode. Instead I found a bug in CoreData

If you'd like to follow along the code, the code is available at my github:
https://github.com/gietal/AppleSandbox/tree/master/CoreData/CoreDataPlayground

Quick overview of CoreData, it stores your data in a sqlite database which live in your App's "Application Support" folder under ~/Library/<Your app bundle identifier> or ~/Library/Containers/<Your app bundle identifier>/Data/Library for sandboxed apps. You can store different types of data with CoreData such as string, int, float, bool, and even binary blob. When storing binary blob, you have an option to store it on an external storage:
Picture
​This is useful for bigger binary blob, as it’s less efficient to store and fetch a big binary blob form the sqlite database. If you try to store a big binary blob (bigger than 100kb), and you have that option enabled, CoreData will automatically decide to store your blob into a hidden folder called <Your DB name>_Support/_EXTERNAL_DATA
Picture
​The blob will be stored as a file named with a random UUID, and that UUID is what’s then stored in the sqlite database. You can examine your sqlite database content with DB Browser for SQLite (free from https://sqlitebrowser.org). You can see that the value for my binary blob attribute is the UUID of the file under _EXTERNAL_DATA. This is how CoreData manages your big binary blob.
Picture
Picture
The value of the binary attribute in the sqlite DB is actually the UUID name of the file on the _EXTERNAL_DATA folder
​When you update your binary blob from your code, then the blob file under _EXTERNAL_DATA is also overwritten with the new blob. However, this is not the case in macOS Mojave 10.14.0, which is demonstrated by my sample project

The sample project is quite simple, I have have an Entity with 2 simple attributes: an id (string) and an image (binary blob) with “Allows External Storage” selected. The id is so that I can fetch the same instance of the entity. When you run the app, you will see a window with a save button. 
Picture
Picture
Each time you hit the save button, it will try to save the displayed image as a binary blob to the entity’s instance in the sqlite database. Since the binary blob is greater than 100kb, it will store it under:
~/Library/Containers/com.gietal.sandbox.CoreDataPlayground/Data/Library/Application Support/CoreDataPlayground/.CoreDataPlayground_SUPPORT/_EXTERNAL_DATA

If you hit the save button multiple times, it will try to overwrite the binary blob with the same image every time. Now here’s where the problem lies. On macOS High Sierra (10.13.6) there is no issue with this, the binary blob is overwritten correctly. On macOS Mojave (10.14.0) however, this is not the case.

When you try running the same app on macOS Mojave, and start hitting the save button multiple times, you will observe that a new file keeps getting added under the _EXTERNAL_DATA folder. This is a serious problem as overtime the amount of useless files keep getting piled up inside this hidden folder. Normal non-savvy users would never know how their mac ran out of space so quickly due to this issue.

I submitted this issue to Apple’s Bug Reporter, and they acknowledged that it was a bug and are working on a fix. Hopefully macOS Mojave 10.14.1 come out soon with a fix for this.

A workaround you can do at the moment is to save the entity’s instance with a nil value for the binary attribute, and save it so that CoreData delete the external file. Then, you can save the instance again with the proper binary blob.
// magic functions to get the CoreData context and entity's instance
let context = getContext()
let entity = getEntity()

// nil out the binary blob, so CoreData erases the file in _EXTERNAL_DATA
entity.setValue(nil, forKey: "image")
context.save()

// then actually save your new blob
entity.setValue(blob, forKey: "image")
context.save()
0 Comments

custom NSOutlineView disclosure button

9/1/2018

0 Comments

 
NSOutlineView is a very nice template to create a Finder-like table view of your collection. However, as any other things Cocoa, it is very easy to create UI that looks "standard", but it's not that straight forward to customize it.

For instance, right out of the box, NSOutlineView already looks like the Finder app, complete with the collapse button (disclosure button is what they call it).
Picture
​This is great to start with, but now what if I don't like the default triangle disclosure button? What if I want to use my own image for the button? One would think that there must be an image/alternate attribute for the disclosure button or something, but no... there isn't.

So we have to devise a roundabout way to customize the button..

Here's how you can customize the disclosure button

​First, we have to add a new button to the expandable column of your outline view, usually the first (left-most) column. In my case, it's the "Hostname" column.
Picture

Change the button type to "Toggle"
Picture

Put your desired images on these fields:
  • Image: The image when the disclosure button is expanded (down arrow)
  • Alternate: The image when the disclosure button is closed (right arrow)
I'm using what the framework provides here, but you can put whatever image you want from your asset catalog
Picture

​Now, add an identifier to this button so that we can create this button with the identifier. Let's name it as "MyDisclosureButton".
Picture
​
​Now, NSOutlineView create its views by calling makeView(), providing the appropriate identifier for each elements on the table. In order to create our own button, we have to subclass NSOutlineView and override this function. When NSOutlineView is about to make the disclosure button, we can intercept this call by checking the identifier that it sent.
class GNLOutlineView: NSOutlineView {
  override func makeView(withIdentifier identifier: NSUserInterfaceItemIdentifier, owner: Any?) -> NSView? {

    // intercept disclosure button creation
    if identifier == NSOutlineView.disclosureButtonIdentifier {
      // then create our custom button
      let myView = super.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "MyDisclosureButton"), owner: owner) as! NSButton
      
      // we need to change the identifier so that the outline view knows to use this view as the disclosure button
      myView.identifier = identifier      
      return myView
    }

    // for everything else, use the default function
    return super.makeView(withIdentifier: identifier, owner: owner)
  }
}

​Change the outline view to use GNLOutlineView as the custom class from the interface builder. When we build and run we should get this:
Picture

​​Great! We can now display custom disclosure button for our outline view! 

But wait a second... when we click on our disclosure button, nothing happen! The group doesn't collapse, and the button doesn't change its image. It would seem like we didn't set the target+action properties on our button, so the button doesn't do anything when it's clicked. 

Where should we point the target+action of our button to? Let's cheat and take a look at where the original disclosure button's target+action pointed to. After all, they were already working without us explicitly modifying anything, so it must be pointing to the correct target+action!

We can create the original disclosure button by calling the super's implementation of the makeView function
// get the original disclosure button
let originalButton = super.makeView(withIdentifier: identifier, owner: owner) as! NSButton
If we stop the debugger after the creation of the original disclosure button, we can inspect the values of the button's target+action (if you drill down enough)
Picture
It would appear that the target is just our outline view, but the action is more interesting. The action points to a function called _outlineControlClicked. Since we don't see this function on NSOutlineView AND it starts with an underscore, it is highly likely that it is a private function (an Apple engineer I met at WWDC 2018 told me about the underscore prefixes on private functions and properties).

A simple solution to this is to assign our button's target+action to the target+action of the original button.
myView.target = originalButton.target
myView.action = originalButton.action

Now, after we hook our button's target+action, we can see that our disclosure button now works perfectly!
0 Comments

Main and Key Windows Shenanigans

8/18/2018

0 Comments

 
Not sure if this is the case in general, or just some corner case that I hit while being unlucky.

What I found is that when an app becomes active, there has to be 1 main window on the same space as the current key window. However, this only happens to me when Displays Have Separate spaces is enabled while having multi monitor setup:
Picture
​My specific scenario:
  • Have 2 monitors A and B
  • Create an app with 2 windows:
    • Window 1: can become main window and key window
    • Window 2: can become key window, cannot become main window
  • Move Window 1 to Monitor A
  • Move Window 2 to Monitor B
  • Deactivate the app by going to another app (Finder or something)
  • Click Window 2 on Monitor B to activate the app

Expectation:
  • Window 2 is activated and is the key window (have highlight on the window title bar)
Actual observed behavior:
  • Window 2 is activated
  • Then immediately after that, Window 1 is activated

This produce a jarring effect where clicking on a window actually activates another window and shift the active window to that window instead.

One solution is to make Window 2 to be able to become main window, but there might be a case where you might not want that.
0 Comments

NSWindow positioning shenanigans

8/4/2018

0 Comments

 
You're playing around with NSWindow, and you find this function called setFrame, setFrameOrigin, and setFrameTopLeftPoint. You feel like you are the king of the world when the thought of being able to move your window anywhere programmatically consume you. Except that when you realized that Cocoa stops you from doing that.

Cocoa allows you to move the window anywhere using those functions, except when your window starts going above the menu bar.

For instance, consider the following scenario:
  • Screen Configuration:
    • Resolution at 1280 x 800
    • Menu bar height = 22 pixel
    • Which means, the menu bar starts at y = 778 (Cocoa coordinate start at the bottom left)
  • Your Window:
    • Size is 100 x 100

When you call window.setFrameTopLeftPoint with the position of (x: 100, y: 800) your window's top left will actually be positioned at (x: 100, y: 778), right under the menu bar.

Same goes with the other setFrame* variants, your window will be pushed down until no part of your window is above the menu bar.

This is of course unless your window is the size (or bigger) than the screen, in which case it has no choice.
0 Comments

Launch macos app from URL

7/21/2018

0 Comments

 
If you want to launch your app from a URL, Cocoa provided a nice way of doing it. 

First, you have to register for the event. You can do it anytime, but I put this code on my AppDelegate class:
open func applicationWillFinishLaunching(_ notification: Notification) {
    initializeURIOptions()
}

fileprivate func initializeURIOptions() {
    // register to listen to the url event
    let appleEventManager = NSAppleEventManager.shared()
    appleEventManager.setEventHandler(self, andSelector: #selector(handleGetURLEvent(event:withReplyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}
Then, we have to actually handle the URL parameters. In this example, we assume that the URL is of the form myapp://param1=val1&param2=val2

So we must parse the URL and extract the "param=value" pairs
@objc fileprivate func handleGetURLEvent(event: NSAppleEventDescriptor?, withReplyEvent replyEvent: NSAppleEventDescriptor?) {
    guard let event = event else {
        return
    }
    // our URL event is in this format:
    // myapp://param1=val1&param2=val2&paramN=valN
    
    // grab the query pairs, that is the pairs separated by '&'
    guard let paramDesc = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue,
        let url = NSURL(string: paramDesc),
        let queryPairs = url.query?.components(separatedBy: "&") else {
            return
    }
    
    // parse the key/value pair
    // querypairs is assumed to be in the form of "key=value"
    var settingPairs = [String: String]()
    for query in queryPairs {
        let kvp = query.components(separatedBy: "=")
        if kvp.count != 2 {
            continue
        }
        
        let key = kvp[0].lowercased()
        let value = kvp[1].lowercased()
        settingPairs[key] = value
    }
    
    // do stuff with settingPairs
}
Finally, we have to let the OS know to associate the "myapp" URL scheme with our app. 

​We can do this by editing the app's info.plist and adding a URL Schemes item under the URL types item. The value for this key is the scheme that the OS will associate with your app.
Picture
Now, you should be able to launch your app by going to Safari (or any web browser and Terminal), and typing myapp://

If you want to pass parameters to the app you can simply add them to the URL myapp://?param1=val1&param2=val2

When you do this from Safari, you will get a notification such as this:
Picture
To debug an app that is launched this way, you can modify XCode's setting to wait until your app is launched before attaching the debugger to the app.
Picture
Picture
Picture
XCode will now automatically attach the debugger to your app when you launch your app via the URL myapp://
0 Comments

NSSecureTextField Shenanigans

7/14/2018

1 Comment

 
Interesting finding:

From the get go, you're not able to input accented characters with keyboard shortcut (alt+e e, alt+u o, etc) on a NSSecureTextField.
But I find this interesting "feature" (maybe a bug or loophole in Apple's implementation of secure text field?) which allows you to do just that.

Here's what to do:

Make sure key repeat is on at System Preference > Keyboard > Keyboard
Picture
I created a simple app with a basic NSSecureTextField and a label to display the content of the secure text field.

​Go to the NSSecureTextField
​Hold e (or any letter that has accented character), until the key repeats
Picture
After this point, key combinations (alt+e e, alt+y, alt+u o, etc) will work as expected ​
Picture
1 Comment

NSPopUpButton shenanigans

7/8/2018

0 Comments

 
​action/value bound to NSPopupButton is only triggered when user changes the value from the UI. It doesnt get triggered when you change the value programmatically.

For example, if we bind the NSPopupButton's selected index property:
​
Picture
internal var selectedIndex: AnyObject! {
    didSet {
        let oldIndex = oldValue as? Int
        let newIndex = selectedIndex as? Int
        print("selectedIndex didSet, old: \(oldIndex), new: \(newIndex)")
    }
}
changing the selected value via the UI will trigger selectedIndex.didSet as expected
Picture
However, when we change the index programmatically such as this:
popUpButton.selectItem(at: 2)
Doing so will change the value in the UI, but it won't trigger selectedIndex.didSet as expected.

A bigger issue happens when we have this scenario:
  • ​User selects index 1 from the UI
  • We programmatically call popupButton.selectItem(at: 2), the UI changed to show that index 2 is selected
  • User reselects index 1 from the UI
  • selectedIndex.didset is not called for the user action!!
       
This happens because when we change the selected item programmatically to 2, selectedIndex's value doesn't actually change, it stayed at 1. So when the user change the index back to 1 via the UI, Cocoa realized that the value of selectedIndex is still the same (1), so it decided not to call the didset function.
0 Comments

UserDefaults shenanigans

9/7/2017

0 Comments

 
It seems like the UserDefaults or at least the shared group UserDefaults is being cached somewhere, and it can put you in a confused debugging state.

To highlight what I meant, create 2 sandboxed apps with the same app group, i.e: com.gietal.sandbox.group

On the first app, do the following:
// App 1
if let defaults = UserDefaults(suiteName: "com.gietal.sandbox.group") {
    defaults.set(123, forKey: "myKey")
}
When you run the first app, it should create this file ~/Library/Group Containers/com.gietal.sandbox.group/Library/Preferences/com.gietal.sandbox.group.plist where the setting "myKey = 123" is stored as xml.

​Now on the second app, try to read the value from the shared group UserDefaults. At this point, it should return the expected value of 123
// App 2
if let defaults = UserDefaults(suiteName: "com.gietal.sandbox.group") {
    let value = defaults.value(forKey: "myKey")
    print("value: \(value)")
}
Now delete the plist file ~/Library/Group Containers/com.gietal.sandbox.group/Library/Preferences/com.gietal.sandbox.group.plist

And try reading the value again from app 2. You would be surprised that it would still returns 123 instead of the expected nil.

However, if you reboot your mac and try reading the value of "myKey" from app 2 again, it will correctly returns nil this time!

So this made me believe that UserDefaults is being cached somewhere in the system and could get out of sync with the actual plist file where the values are stored.
0 Comments

xcodebuild couldn't posix_spawn: error 7

7/8/2017

0 Comments

 
​Tldr: path to the working directory was too long
 
When building an older project with xcodebuild, I suddenly started getting this error:

** INTERNAL ERROR: Uncaught exception **
Uncaught Exception: Couldn't posix_spawn: error 7
Stack:
  0   __exceptionPreprocess (in CoreFoundation)
  1   objc_exception_throw (in libobjc.A.dylib)
  2   +[NSException raise:format:] (in CoreFoundation)
  3   -[NSConcreteTask launchWithDictionary:] (in Foundation)
  4   __46-[IDEShellScriptExecutionActionOperation main]_block_invoke_2.229 (in IDEStandardExecutionActionsCore)
  5   ___DVTAsyncPerformBlockOnMainRunLoop_block_invoke (in DVTFoundation)
  6   __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ (in CoreFoundation)
  7   __CFRunLoopDoBlocks (in CoreFoundation)
  8   __CFRunLoopRun (in CoreFoundation)
  9   CFRunLoopRunSpecific (in CoreFoundation)
10   CFRunLoopRun (in CoreFoundation)
11   -[Xcode3CommandLineBuildTool _buildWithTimingSection:] (in Xcode3Core)
12   -[Xcode3CommandLineBuildTool run] (in Xcode3Core)
13  0x000000010fe00202 (in xcodebuild)
14   start (in libdyld.dylib)

After much googling and investigating, I found that:
  • xcodebuild calls posix_spawn()
  • posix_spawn() calls exec()
  • exec() fail with error 7 – E2BIG: Argument list too long
  • running getconf ARG_MAX from the terminal shows that the maximum argument list was 262144 bytes
 
Originally I ran the script from ~/jenkinsroot/some/very/long/path/to/the/project/directory
and the individual files that are being compiled lies way deeper on the folder structure. Which made me to believe that when xcodebuild tries to give the files full path as an argument to posix_spawn, the length exceeds 262144 bytes

It works when I move the project root directory to ~/shorter/path
 
helpful links:
http://lxr.free-electrons.com/source/include/uapi/asm-generic/errno-base.h#L14
http://www.in-ulm.de/~mascheck/various/argmax/
http://pubs.opengroup.org/onlinepubs/009695399/functions/posix_spawn.html
http://pubs.opengroup.org/onlinepubs/009695399/functions/exec.html

 
now I can have a peaceful weekend :)
0 Comments
<<Previous

    Archives

    September 2018
    August 2018
    July 2018
    September 2017
    July 2017
    November 2016
    October 2016
    September 2016
    June 2016
    May 2016
    March 2016

    Categories

    All
    Shenanigans
    Software Engineering
    Swift
    Vr
    Xcode

    RSS Feed

Powered by Create your own unique website with customizable templates.