How Flutter embeds into macOS (and how GoFlow will do the same).
Flutter's approach:
- Native Swift app shell in
macos/directory - Dart code stays in
lib/directory (separate!) - Build process compiles Dart → App.framework
- Native shell loads Flutter.framework + App.framework at runtime
- No Dart code in macos/ - it's all configuration and native wrapper
GoFlow will do the same:
- Native Swift shell in
macos/ - Go code in
lib/ - Build compiles Go → libapp.dylib
- Shell loads GoFlowRuntime.framework + libapp.dylib
- Clean separation, proper .app bundles
demo_flutter_for_template/
├── lib/
│ └── main.dart # ← Dart code lives here!
│
├── macos/ # ← No Dart code here!
│ ├── Runner/
│ │ ├── AppDelegate.swift # App entry
│ │ ├── MainFlutterWindow.swift # Window setup
│ │ ├── Info.plist
│ │ └── Assets.xcassets
│ │
│ ├── Flutter/
│ │ ├── Flutter-Debug.xcconfig # #include ephemeral
│ │ ├── Flutter-Release.xcconfig # #include ephemeral
│ │ ├── GeneratedPluginRegistrant.swift
│ │ └── ephemeral/ # Auto-generated
│ │ └── Flutter-Generated.xcconfig
│ │
│ └── Runner.xcodeproj/
│
└── build/ # Build output
└── macos/
└── Build/Products/Release/
└── demo_flutter_for_template.app/
├── Contents/
│ ├── MacOS/Runner # Native exe
│ └── Frameworks/
├── FlutterMacOS.framework # Engine
└── App.framework # Dart code!
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}What happens:
- Inherits from
FlutterAppDelegate(provided by FlutterMacOS.framework) - FlutterAppDelegate handles all Flutter initialization
- No manual Flutter engine setup needed
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
// Create Flutter view controller
let flutterViewController = FlutterViewController()
// Set as window content
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
// Register plugins
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}What happens:
FlutterViewControlleris provided by FlutterMacOS.framework- It automatically:
- Loads lib/main.dart (path from xcconfig)
- Starts Dart VM
- Renders Flutter UI
- Handles events
// Auto-generated by Flutter build
FLUTTER_ROOT=/Users/user/flutter
FLUTTER_APPLICATION_PATH=/path/to/project
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
What it does:
- Tells Xcode where Flutter is installed
- Specifies project path
- Build metadata
# flutter build macos
Step 1: Compile Dart code
lib/main.dart → App.framework
Step 2: Run Xcode build
xcodebuild -project Runner.xcodeproj
Step 3: Xcode links frameworks
- FlutterMacOS.framework (engine)
- App.framework (your code)
Step 4: Create .app bundle
Runner.app/
Contents/
MacOS/Runner
Frameworks/
FlutterMacOS.framework
App.framework ← All Dart code is here!mygoflowapp/
├── lib/
│ └── main.go # ← Go code lives here!
│
├── macos/ # ← No Go code here!
│ ├── Runner/
│ │ ├── AppDelegate.swift
│ │ ├── MainGoFlowWindow.swift
│ │ ├── Info.plist
│ │ └── Assets.xcassets
│ │
│ ├── GoFlow/
│ │ ├── GoFlow-Debug.xcconfig
│ │ ├── GoFlow-Release.xcconfig
│ │ └── ephemeral/
│ │ └── GoFlow-Generated.xcconfig
│ │
│ └── Runner.xcodeproj/
│
└── build/
└── macos/
└── Build/Products/Release/
└── mygoflowapp.app/
├── Contents/
│ ├── MacOS/Runner
│ └── Frameworks/
├── GoFlowRuntime.framework # Our engine
└── libapp.dylib # Go code!
import Cocoa
import GoFlowRuntime
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Load main window from storyboard
// Window will be configured by MainGoFlowWindow
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func applicationWillTerminate(_ notification: Notification) {
// Cleanup GoFlow runtime
GoFlowRuntime.shutdown()
}
}import Cocoa
import GoFlowRuntime
class MainGoFlowWindow: NSWindow {
var goflowViewController: GoFlowViewController!
override func awakeFromNib() {
// Create GoFlow view controller
goflowViewController = GoFlowViewController()
// Configure window
let windowFrame = self.frame
self.contentViewController = goflowViewController
self.setFrame(windowFrame, display: true)
// Load Go app
// Path to libapp.dylib is determined by build system
goflowViewController.loadApp()
super.awakeFromNib()
}
}GoFlowRuntime.framework/
├── Versions/
│ └── A/
│ ├── GoFlowRuntime # Compiled framework
│ ├── Headers/
│ │ ├── GoFlowRuntime.h
│ │ ├── GoFlowViewController.h
│ │ └── GoFlowView.h
│ └── Resources/
│ └── Info.plist
└── GoFlowRuntime -> Versions/Current/GoFlowRuntime
Framework provides:
// Public API
public class GoFlowViewController: NSViewController {
public func loadApp()
public func reload()
public func handleEvent(_ event: NSEvent)
}
public class GoFlowView: NSView {
// Renders Go UI
}
public class GoFlowRuntime {
public static func initialize()
public static func shutdown()
}
// Generated by: goflow build macos
GOFLOW_ROOT=/usr/local/goflow
GOFLOW_APP_PATH=$(PROJECT_DIR)/lib/main.go
GOFLOW_BUILD_DIR=build
GOFLOW_APP_NAME=mygoflowapp
GOFLOW_BUILD_NUMBER=1
GOFLOW_VERSION=0.2.0
// Framework paths
GOFLOW_RUNTIME_PATH=$(GOFLOW_ROOT)/runtime/macos/GoFlowRuntime.framework
GOFLOW_APP_DYLIB=$(BUILT_PRODUCTS_DIR)/libapp.dylib
// Linking
FRAMEWORK_SEARCH_PATHS=$(inherited) $(GOFLOW_ROOT)/runtime/macos
LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/../Frameworks
# goflow build macos
Step 1: Compile Go code to shared library
go build -buildmode=c-shared \
-ldflags="-s -w" \
-o build/macos/libapp.dylib \
lib/main.go
Step 2: Generate xcconfig
echo "GOFLOW_APP_DYLIB=libapp.dylib" > macos/GoFlow/ephemeral/GoFlow-Generated.xcconfig
Step 3: Run Xcode build
xcodebuild \
-project macos/Runner.xcodeproj \
-scheme Runner \
-configuration Release \
OBJROOT=build/macos/obj \
SYMROOT=build/macos/sym
Step 4: Copy frameworks and dylib
cp build/macos/libapp.dylib \
build/macos/Build/Products/Release/Runner.app/Contents/Frameworks/
Step 5: Sign and create .app
codesign -s - --deep \
build/macos/Build/Products/Release/Runner.apppackage main
import "C"
import (
"github.com/base-go/GoFlow/pkg/core"
"github.com/base-go/GoFlow/pkg/widgets"
)
// Export for CGo - called by GoFlowRuntime.framework
//export GoFlowMain
func GoFlowMain() {
app := core.NewApp()
app.Run(func() core.Widget {
return widgets.MaterialApp(
home: MyHomePage(),
)
})
}
//export GoFlowHandleEvent
func GoFlowHandleEvent(eventType C.int, data unsafe.Pointer) {
// Handle events from Swift
}
func main() {} // Required for c-shared
type MyHomePage struct {
widgets.StatefulWidget
}
func (h *MyHomePage) CreateState() core.State {
return &MyHomePageState{}
}
type MyHomePageState struct {
widgets.State
counter int
}
func (s *MyHomePageState) Build(ctx core.BuildContext) core.Widget {
return widgets.Scaffold(
appBar: widgets.AppBar(
title: widgets.Text("GoFlow Demo"),
),
body: widgets.Center(
child: widgets.Column(
children: []core.Widget{
widgets.Text("You have pushed the button this many times:"),
widgets.Text(
strconv.Itoa(s.counter),
style: core.TextStyle{FontSize: 32},
),
},
),
),
floatingActionButton: widgets.FloatingActionButton(
onPressed: func() {
s.SetState(func() {
s.counter++
})
},
child: widgets.Icon(Icons.Add),
),
)
}import Cocoa
import Metal
public class GoFlowViewController: NSViewController {
private var goflowView: GoFlowView!
private var appHandle: UnsafeMutableRawPointer?
public override func loadView() {
goflowView = GoFlowView(frame: NSRect(x: 0, y: 0, width: 800, height: 600))
self.view = goflowView
}
public override func viewDidLoad() {
super.viewDidLoad()
loadApp()
}
public func loadApp() {
// Load the Go shared library
guard let appPath = Bundle.main.path(forResource: "libapp", ofType: "dylib") else {
print("Error: libapp.dylib not found")
return
}
// Open shared library
guard let handle = dlopen(appPath, RTLD_LAZY) else {
print("Error loading libapp.dylib")
return
}
appHandle = handle
// Get GoFlowMain symbol
guard let mainFunc = dlsym(handle, "GoFlowMain") else {
print("Error: GoFlowMain symbol not found")
return
}
// Call Go main function
typealias GoFlowMainType = @convention(c) () -> Void
let goMain = unsafeBitCast(mainFunc, to: GoFlowMainType.self)
goMain()
// Start render loop
goflowView.startRenderLoop()
}
public func reload() {
// For hot reload: reload the dylib
if let handle = appHandle {
dlclose(handle)
}
loadApp()
}
deinit {
if let handle = appHandle {
dlclose(handle)
}
}
}import Cocoa
import Metal
public class GoFlowView: NSView {
private var metalDevice: MTLDevice!
private var metalLayer: CAMetalLayer!
private var displayLink: CVDisplayLink?
public override init(frame: NSRect) {
super.init(frame: frame)
setupMetal()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupMetal()
}
private func setupMetal() {
// Setup Metal rendering
metalDevice = MTLCreateSystemDefaultDevice()
metalLayer = CAMetalLayer()
metalLayer.device = metalDevice
metalLayer.pixelFormat = .bgra8Unorm
self.layer = metalLayer
self.wantsLayer = true
}
public func startRenderLoop() {
// Create display link for 60fps rendering
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
if let displayLink = displayLink {
CVDisplayLinkSetOutputCallback(displayLink, { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, context) -> CVReturn in
// Call Go rendering function
autoreleasepool {
self.render()
}
return kCVReturnSuccess
}, Unmanaged.passUnretained(self).toOpaque())
CVDisplayLinkStart(displayLink)
}
}
private func render() {
// Get drawable from Metal layer
guard let drawable = metalLayer.nextDrawable() else { return }
// Call Go render function (from libapp.dylib)
// GoFlowRender() will draw the UI
drawable.present()
}
}myapp/
├── lib/
│ └── main.go # Default app template
├── macos/
│ ├── Runner/
│ │ ├── AppDelegate.swift
│ │ ├── MainGoFlowWindow.swift
│ │ ├── Info.plist
│ │ ├── Assets.xcassets/
│ │ └── Base.lproj/
│ │ └── MainMenu.storyboard
│ ├── GoFlow/
│ │ ├── GoFlow-Debug.xcconfig
│ │ └── GoFlow-Release.xcconfig
│ └── Runner.xcodeproj/
└── go.mod#!/bin/bash
# Executed when user runs: goflow build macos
# 1. Create build directories
mkdir -p build/macos/Frameworks
# 2. Compile Go app to shared library
echo "Compiling Go code..."
go build \
-buildmode=c-shared \
-ldflags="-s -w" \
-o build/macos/Frameworks/libapp.dylib \
lib/main.go
# 3. Generate xcconfig
cat > macos/GoFlow/ephemeral/GoFlow-Generated.xcconfig <<EOF
GOFLOW_ROOT=/usr/local/goflow
GOFLOW_APP_DYLIB=\$(BUILT_PRODUCTS_DIR)/../../../Frameworks/libapp.dylib
GOFLOW_RUNTIME_PATH=\$(GOFLOW_ROOT)/runtime/macos
FRAMEWORK_SEARCH_PATHS=\$(inherited) \$(GOFLOW_RUNTIME_PATH)
EOF
# 4. Build with Xcode
echo "Building macOS app..."
xcodebuild \
-project macos/Runner.xcodeproj \
-scheme Runner \
-configuration Release \
-derivedDataPath build/macos
# 5. Copy dylib to app bundle
cp build/macos/Frameworks/libapp.dylib \
build/macos/Build/Products/Release/Runner.app/Contents/Frameworks/
# 6. Code sign
codesign -s - --deep --force \
build/macos/Build/Products/Release/Runner.app
echo "✓ Build complete: build/macos/Build/Products/Release/Runner.app"build/macos/Build/Products/Release/
└── myapp.app/
└── Contents/
├── Info.plist
├── MacOS/
│ └── Runner # Native Swift executable
├── Frameworks/
│ ├── GoFlowRuntime.framework # Our runtime engine
│ └── libapp.dylib # Your Go code!
└── Resources/
└── Assets.car
Double-click myapp.app → Runs your Go GUI app!
Since Go is compiled to a shared library (.dylib), hot reload is straightforward:
class GoFlowViewController {
func reload() {
// 1. Close current dylib
if let handle = appHandle {
dlclose(handle)
}
// 2. Recompile Go code
// goflow triggers: go build -buildmode=c-shared lib/main.go
// 3. Reload dylib
appHandle = dlopen(newDylibPath, RTLD_LAZY)
// 4. Call GoFlowMain again
let mainFunc = dlsym(appHandle, "GoFlowMain")
unsafeBitCast(mainFunc, to: GoFlowMainType.self)()
// 5. Trigger redraw
goflowView.setNeedsDisplay()
}
}- Swift/macOS code in
macos/ - Go app code in
lib/ - No mixing of languages in same directory
- Creates proper .app bundle
- Works with Xcode
- Can submit to Mac App Store
- Follows Apple guidelines
- Reload .dylib without restarting app
- Fast iteration
- State preservation possible
- Open macos/Runner.xcodeproj in Xcode
- Build/run/debug from Xcode
- Or use
goflow run macos
- Standard .app bundle
- Code signing works
- Notarization compatible
- DMG creation standard
-
Create GoFlowRuntime.framework
- Window management
- Metal rendering
- Event handling
- CGo bridge to Go code
-
Template for macos/ directory
- AppDelegate.swift
- MainGoFlowWindow.swift
- Xcode project
- Storyboards
-
Build script integration
goflow newgenerates macos/goflow build macoscompiles and linksgoflow run macoslaunches app
-
Hot reload
- File watcher
- Recompile Go → .dylib
- Reload in running app
Summary: Flutter's macOS folder is just a thin native wrapper that loads the Flutter framework and compiled Dart code at runtime. GoFlow can do exactly the same with a Swift wrapper loading our runtime and compiled Go code. This gives us proper macOS integration, .app bundles, and hot reload.