Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add example of using iOS 17+ interactive widgets in Titanium #1

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions app/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,32 @@ import TiWidgetKit from 'ti.widgetkit';
const GROUP_IDENTIFIER = 'group.io.tidev.sample-widgetkit';
const USER_DEFAULTS_IDENTIFIER = 'kSampleAppMyData';

const userDefaults = Ti.App.iOS.createUserDefaults({ suiteName: GROUP_IDENTIFIER });

function onWindowFocus() {
refreshUI();
}

function refreshUI() {
const data = userDefaults.getObject(USER_DEFAULTS_IDENTIFIER);

$.title.text = data.title;
$.count.text = `${data.count}`;
}

function saveData() {
const payload = { title: 'My Title', count: 1337 };
const data = userDefaults.getObject(USER_DEFAULTS_IDENTIFIER);
const payload = { title: data.title, count: data.count + 1 };

// 1) Save data
const userDefaults = Ti.App.iOS.createUserDefaults({ suiteName: GROUP_IDENTIFIER });
userDefaults.setObject(USER_DEFAULTS_IDENTIFIER, payload);

// 2) Refresh widget UI
TiWidgetKit.reloadAllTimelines();
refreshUI();

// NOTE: The widget will show the "Default" title until your app groups are properly linked to your
// main app and extension. Please read this article very carefully before jumping into more detail:
// NOTE: Please read this article very carefully before jumping into more detail:
// https://developer.apple.com/documentation/widgetkit/creating-a-widget-extension

alert('Done!');
}

$.getView().open();
8 changes: 6 additions & 2 deletions app/views/index.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<Alloy>
<NavigationWindow>
<Window title="Widget Kit Sample App">
<Button title="Save data to widget" onClick="saveData" />
<Window title="Widget Kit Sample App" onFocus="onWindowFocus">
<View height="160" width="200" borderRadius="15" backgroundColor="#f0f0f0" layout="vertical">
<Label font.fontWeight="bold" font.fontSize="18" top="30" id="title" />
<Label id="count" />
<Button title="Increment counter" top="20" onClick="saveData" />
</View>
</Window>
</NavigationWindow>
</Alloy>
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1340;
LastUpgradeCheck = 1340;
LastUpgradeCheck = 1500;
TargetAttributes = {
3A3F7EEC286657C6005E692E = {
CreatedOnToolsVersion = 13.4.1;
Expand Down Expand Up @@ -156,12 +156,70 @@
3A3F7EE728665782005E692E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
ONLY_ACTIVE_ARCH = YES;
};
name = Debug;
};
3A3F7EE828665782005E692E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
};
name = Release;
};
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import WidgetKit
import SwiftUI
import Intents
import AppIntents

// Change to your data - this struct is just a type definition that should
// have the same structure as your saved object in your Ti.App.iOS.UserDefaults
Expand All @@ -16,15 +17,15 @@ struct MyData: Codable {
let count: Int
}

private func fetchData() -> MyData? {
// Change to your group that is assigned in your app and extension (via "Signing and Capabilities" > "App Groups"
// NOTE: Your app has to have the same app group and on device, your provisioning profile should include it as well
let GROUP_IDENTIFIER = "group.io.tidev.sample-widgetkit"
let USER_DEFAULTS_IDENTIFIER = "kSampleAppMyData"
// Change to your group that is assigned in your app and extension (via "Signing and Capabilities" > "App Groups"
// NOTE: Your app has to have the same app group and on device, your provisioning profile should include it as well
let GROUP_IDENTIFIER = "group.io.tidev.sample-widgetkit"
let USER_DEFAULTS_IDENTIFIER = "kSampleAppMyData"

private func fetchData() -> MyData? {
let defaults = UserDefaults(suiteName: GROUP_IDENTIFIER)

if let archivedData = defaults?.object(forKey: USER_DEFAULTS_IDENTIFIER) as? Data, let rawData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(archivedData) as? [String: Any],
if let rawData = defaults?.object(forKey: USER_DEFAULTS_IDENTIFIER) as? [String: Any],
let rawDataJSON = try? JSONSerialization.data(withJSONObject: rawData, options: .fragmentsAllowed),
let data = try? JSONDecoder().decode(MyData.self, from: rawDataJSON) {
return data
Expand All @@ -36,12 +37,18 @@ private func fetchData() -> MyData? {
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
let data = fetchData()
return SimpleEntry(date: Date(), title: data?.title ?? "Default", configuration: ConfigurationIntent())
return SimpleEntry(date: Date(),
count: data?.count ?? 0,
title: data?.title ?? "Counter",
configuration: ConfigurationIntent())
}

func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let data = fetchData()
let entry = SimpleEntry(date: Date(), title: data?.title ?? "Default", configuration: configuration)
let entry = SimpleEntry(date: Date(),
count: data?.count ?? 0,
title: data?.title ?? "Counter",
configuration: configuration)
completion(entry)
}

Expand All @@ -53,7 +60,10 @@ struct Provider: IntentTimelineProvider {
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, title: data?.title ?? "Default", configuration: configuration)
let entry = SimpleEntry(date: entryDate,
count: data?.count ?? 0,
title: data?.title ?? "Counter",
configuration: configuration)
entries.append(entry)
}

Expand All @@ -64,6 +74,7 @@ struct Provider: IntentTimelineProvider {

struct SimpleEntry: TimelineEntry {
let date: Date
let count: Int
let title: String
let configuration: ConfigurationIntent
}
Expand All @@ -74,6 +85,12 @@ struct SampleWidgetExtensionEntryView : View {
var body: some View {
Text(entry.title).fontWeight(.bold)
Text(entry.date, style: .time)
Text("\(entry.count)")
if #available(iOS 17.0, *) {
Button(intent: ButtonCounter()) {
Image(systemName: "bolt.fill")
}
}
}
}

Expand Down Expand Up @@ -110,7 +127,26 @@ func supportedWidgetFamilies() -> [WidgetFamily] {
struct SampleWidgetExtension_Previews: PreviewProvider {
static var previews: some View {
let data = fetchData()
SampleWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), title: data?.title ?? "Default", configuration: ConfigurationIntent()))
SampleWidgetExtensionEntryView(entry: SimpleEntry(date: Date(),
count: data?.count ?? 0,
title: data?.title ?? "Counter",
configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
struct ButtonCounter: AppIntent {

static var title: LocalizedStringResource = "Count Incrementer"
static var description = IntentDescription("Increments the given count!")

func perform() async throws -> some IntentResult {
let lastData = fetchData()

let defaults = UserDefaults(suiteName: GROUP_IDENTIFIER)
defaults?.set(["title": "Counter", "count": (lastData?.count ?? 0) + 1], forKey: USER_DEFAULTS_IDENTIFIER)

return .result()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.tidev.sample-widgetkit</string>
</array>
</dict>
</plist>
2 changes: 1 addition & 1 deletion plugins/ti.alloy/hooks/alloy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Alloy
* Copyright (c) 2012 by Appcelerator, Inc. All Rights Reserved.
* Copyright TiDev, Inc. 04/07/2022-Present
* See LICENSE for more information on licensing.
*/

Expand Down
2 changes: 1 addition & 1 deletion plugins/ti.alloy/hooks/deepclean.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Alloy
* Copyright (c) 2014 by Appcelerator, Inc. All Rights Reserved.
* Copyright TiDev, Inc. 04/07/2022-Present
* See LICENSE for more information on licensing.
*/

Expand Down
10 changes: 9 additions & 1 deletion tiapp.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
</target>
</extension>
</extensions>
<entitlements>
<plist>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.tidev.sample-widgetkit</string>
</array>
</plist>
</entitlements>
<plist>
<dict>
<key>UISupportedInterfaceOrientations~iphone</key>
Expand Down Expand Up @@ -69,7 +77,7 @@
<target device="ipad">true</target>
<target device="iphone">true</target>
</deployment-targets>
<sdk-version>10.1.1.GA</sdk-version>
<sdk-version>12.2.0.v20230520170549</sdk-version>
<plugins><plugin version="1.0">ti.alloy</plugin>
</plugins>
</ti:app>