Repository navigation
[1.0.0] - 2026-10-04
LumiKit 1.0 rebuilds the package as a public UI framework: five products, one naming scheme, a theme value that every component follows live, a Style on every component, built-in strings in four languages, and the iOS 26 APIs adopted with fallbacks for iOS 18. Every 0.x app needs the migration: read docs/MIGRATION-1.0.md, then run Scripts/migrate-1.0.sh <app-dir> --dry-run.
Breaking changes
Products
| Old | New | Why |
|---|---|---|
LumiKitNetwork |
LumiKitDebug |
The network logger and its inspector screens ship in one DEBUG-only product |
Photo browser, grid, crop editor, pick-and-crop coordinator, and share preview in LumiKitUI |
LumiKitPhoto |
Apps that never show a photo no longer link PhotosUI |
LumiKitUI depended on LumiKitNetwork |
LumiKitUI depends on LumiKitCore and SnapKit only |
Link LumiKitDebug yourself, under #if DEBUG |
LMKLottieRefreshControl.animationBundle defaulted to .main and needed an app-supplied JSON |
The package bundles its ring animation, tinted from the theme | Delete the app's copy, or pass animation: for a custom one |
Naming
One naming scheme, documented in CONTRIBUTING.md. The migration script renames all of these.
| Old | New |
|---|---|
LMKConcurrencyHelpers, LMKDateHelper, LMKDateFormatterHelper, LMKFormatHelper, LMKFileUtil |
LMKConcurrency, LMKDate, LMKDateFormat, LMKFormat, LMKFile |
LMKAnimationHelper, LMKHapticFeedbackHelper, LMKDeviceHelper, LMKSceneUtil |
LMKAnimation, LMKHaptics, LMKDevice, LMKScene |
LMKImageUtil, LMKDominantColorExtractor, LMKQRCodeGenerator |
LMKImage (symbol, downsample, dominantColor, qrCode); encodeJPEG(_:maxDimension:quality:) is encodeJPEG(_:maxPixelSize:quality:) and caps in pixels, like downsample |
LMKAlertPresenter, LMKCountdownConfirmation |
LMKAlert |
LMKShareService, LMKPhotoEXIFService, LMKDatePickerHelper, LMKOverscrollFooterHelper |
LMKShare, LMKPhotoMetadata, LMKDatePicker, LMKOverscrollFooterView |
LMKBottomSheetController, LMKCardPageController, LMKCardPanelController, LMKSegmentedPageController |
the same names ending in ViewController |
LMKEnumSelectionBottomSheet |
LMKEnumPicker |
LMKToastType, LMKBannerType |
LMKStatus |
LMKChipStyle, LMKEmptyStateStyle, LMKGradientDirection, LMKDividerOrientation, LMKTipStyle, LMKTipArrowDirection, LMKTextFieldState |
LMKChipView.Variant, LMKEmptyStateView.Layout, LMKGradientView.Direction, LMKDividerView.Orientation, LMKTipView.Placement, LMKTipView.ArrowDirection, LMKValidationState |
LMKGlassView.Style, LMKProgressViewController.Style, LMKSegmentedControl.CornerStyle, LMKActionSheet.ActionStyle |
.Variant, .Mode, .Corners, Action.Style (Style now names each component's style struct) |
LMKDeviceType, LMKScreenSize, LMKButtonRole, LMKTypographyType, LMKShadowConfig |
LMKDevice.Kind, LMKDevice.ScreenSize, LMKButton.Role, LMKTypography.Kind, LMKShadowTheme.Shadow |
LMKPhotoGridContentMode, LMKPhotoGridSortOrder |
LMKPhotoGridViewController.ContentMode, .SortOrder |
LMKRequestData, LMKResponseData |
LMKNetworkRequestRecord.Request, .Response |
LMK*Strings types and lmk*Strings globals |
nested Type.Strings, with Type.strings for the whole app and an instance strings on types you create |
Collection[safe:], String?.nonEmpty, NSAttributedString + |
[lmk_safe:], .lmk_nonEmpty, lmk_appending(_:) |
UIControl.lmk_touchAreaEdgeInsets, lmk_pointInside |
lmk_hitTestInsets, lmk_point(inside:with:) (for your own controls; LumiKit controls keep a 44pt target themselves) |
URLSessionConfiguration.enableNetworkLogging() |
lmk_enableNetworkLogging() |
LMKFileUtil.generateTempFileURL(fileExtension:), clearTmpDirectory() |
LMKFile.temporaryURL(extension:) (non-optional), clearTemporaryFiles(olderThan:matchingPrefix:) |
Theme
- The
LMKThemeprotocol,LMKDefaultTheme, andLMKThemeManagerare gone. Colors are anLMKColorThemestruct with a fully defaulted initializer, andLMKThemeis the whole theme as one value (colors,typography,spacing,cornerRadius,shadow,alpha,layout,animation,extensions) with staticcurrent,apply(_:),update(_:),reset(),observe(_:), andupdates. An app theme isextension LMKTheme { static let myApp = LMKTheme(colors: LMKColorTheme(primary: ...)) }, applied withLMKTheme.apply(.myApp).configure(colors:typography:...)and the per-categoryapply(spacing:)are gone. - Applying a theme re-renders every window:
LMKColor.*values are dynamic colors, and components restyle themselves. Token reads (LMKSpacing.large,LMKColor.primary, ...) work from any isolation, sostatic letdefaults and SwiftUI views can use them. - Color roles:
whitetoonAccent,blacktoscrim,primaryDarktoprimaryVariant,imageBordertooutline,graySofttofillStrong,grayMutedtofill;photoBrowserBackgroundtoLMKPhotoBrowserViewController.Style.backgroundColor. New roles:link,pressedOverlay,selection, andhighContrastBoost(applied to accents under Increase Contrast). LMKAlphais a seven-step ramp with the same values:overlayLighttoxxs,overlayMediumtoxs,overlayDarktosmall,semiTransparenttomedium,overlaytolarge,overlayStrongtoxl,overlayOpaquetoxxl,dimmingOverlaytodimming.LMKShadow.small()/button()/cellCard()/card()/medium()/large()becameLMKShadow.style(for: .level1 ... .level5);LMKShadow.opacitybecameiconOverlayOpacity.LMKAnimation.Durationhas seven steps:buttonPresstoinstant;uiShortandphotoLoadtofast;actionSheetandalerttonormal;modalPresentation,listUpdate,listInsertDelete, andcardExpandtomoderate;screenTransitionanderrorShaketoslow;successFeedbacktoemphasis.SpringandCurveare value types onLMKAnimation.- The factories are gone:
LMKButtonFactory.filled(role:title:)becameLMKButton(title:style: .filled(.role)),LMKLabelFactory.caption(text:)becameUILabel.lmk_make(.caption, text:),LMKCardFactory.cardView()becameLMKCardView(style: .cell).LMKButton.Stylepresets take anLMKButton.Role, not a color (.filled(.destructive));.tint(_:)sets a custom color. - The layout constants (
LMKBottomSheetLayout,LMKCardPageLayout,LMKCardPanelLayout,LMKTipLayout,LMKFloatingButtonLayout,LMKPhotoBrowserConfig,LMKBadgeThemewithLMKBadge) and the overridable appearance properties on the base controllers moved into each component'sStyle; the search bar metrics inLMKLayoutlive onLMKSearchBar.Style.
Presentation
- Anything that presents from a view controller takes
from::LMKAlert,LMKErrorHandler,LMKActionSheet,LMKEnumPicker,LMKDatePicker,LMKShare, the photo browser, sheets, and panels. Views shown inside a hierarchy useshow(in:): toasts, tips, banners, the floating button. Every dismissal isdismiss(), and every completion closure iscompletion:. LMKToast: the tenshowSuccess/showSuccessOnWindowentry points becameLMKToast.show(_ status:_ message:duration:in:completion:),show(LMKToast.Configuration), anddismissAll(in:). With no host, a toast shows on the active window, above presented sheets.LMKBottomSheetViewController.addAsChild(sheet, in:)andLMKCardPanelViewController.show(panel, in:)became the instance methodpresent(from:);dismissSheet()becamedismiss(reason:completion:)anddismissPanel(completion:)becamedismiss(completion:), both safe to call twice;init(cancelTitle:)becameinit(style:)with the title onstrings.animateIn()andanimateOut(velocity:completion:)are no longer public. UIKit'sdismiss(animated:completion:)on a sheet or panel runs the component's own dismissal.LMKActionSheetis a namespace: build anLMKActionSheet.Configurationand callLMKActionSheet.present(_:from:); the view controller isLMKActionSheetViewController. Custom content sizes itself (contentHeight:is gone).LMKEnumPicker.present(from:title:options:selection:onSelect:onCancel:)handles single (T?) and multiple (Set<T>) selection;presentMultiSelectis gone.LMKEnumSelectable.iconNameisString?and defaults tonil.LMKDatePicker.present(_ configuration:from:onConfirm:)withConfiguration,.past(...), and.future(...), pluspresentRange,presentCalendarRange, andpresentWithTextField, replace the seven presenters; every form takes anonCancel.makePicker(_:)builds a picker whose style works under the Mac idiom.LMKAlert.presentTextInput(TextInput, from:onSave:onCancel:),presentActionSheet(actions: [LMKAlert.Action], from:anchor:onCancel:),presentDeleteConfirmation,presentCountdownConfirmation, andconfirm(...) async -> Boolreplace the parameter-heavy forms;LMKErrorHandler.present(from:).confirmandLMKErrorHandler.confirmRetryreturnfalsewhen the alert cannot be shown or goes away without an action.LMKFloatingButtonhas no shared instance: keep the button returned byshow(icon:in:positionKey:onTap:)and calldismiss()on it.LMKShare.present(_ items:from:anchor:completion:)shares images, files, text, and links in one sheet.file(at:)keeps the file unless you passdeletesAfterShare: true(0.xshareFilealways deleted it).
Callbacks
- Closures are named
on<Event>and carry the new value:tapHandlertoonTap,actionHandlertoonAction,dismissHandlertoonDismiss,valueChangedHandlerandstateChangedHandlertoonValueChange,textChangedHandlertoonTextChange,pageChangedHandlertoonPageChange,selectionChangedHandlerandmultiSelectionChangedHandlertoonSelectionChange(Set<Int>),backActiontoonBack.LMKButton.didTapHandleris gone. Two payloads changed:LMKTextField/LMKTextView.onTextChangecarriesString(0.x carriedString?), andLMKCheckboxCell.onValueChangecarries the newisDone(0.xonTogglecarried nothing). Every presenter that can be cancelled offersonCancel. - Single-method delegates became closures:
LMKSearchBarDelegate(onTextChange,onSearch,onBeginEditing,onEndEditing,onCancel),LMKPhotoCropDelegate(onCrop,onCancel),LMKSharePreviewDelegate(onShare,onSave,onFailure). The photo browser and grid data sources and delegates stay. LMKBottomSheetViewController.onDismissTapped()becameonDismiss: ((DismissReason) -> Void)?withcancelButton,dimmingTap,drag,keyCommand, andprogrammaticreasons.
Localization
- Every user-visible default comes from the package's string tables (English, Spanish, Simplified Chinese, Traditional Chinese) through nested
Stringsstructs. Apps that localized LumiKit's strings themselves can delete those overrides; apps in other languages setType.stringsonce at launch, orinstance.stringsper view.
Logging
LMKLogLevelhas six cases,debug,info,notice,warning,error, andfault, so an exhaustiveswitchneeds the two new ones.warningnow logs at the unified log's error type (it was default), andnoticeat default, the lowest type the device keeps.- Debug lines are no longer compiled out of Release builds.
minimumLevelfilters at runtime:.debugby default in DEBUG builds,.infootherwise. A threshold above.erroris treated as.error, so errors and faults are always written. - An attached error is logged as a public summary with its domain and code (
failed [NSURLErrorDomain -1001 <- NSPOSIXErrorDomain 60]) plus its full description as private detail, in place of| Error: <localizedDescription>. LMKLogging.loggained aprivateDetail: String?parameter; a custom conformer adds it.- LumiKit's own lines moved from
.general,.ui,.data,.network, and.errorto the.lumiKitcategory.
Removed
| Removed | Use instead |
|---|---|
LMKToggleButton |
LMKButton with isToggle = true, selectedTitle / selectedImage, onValueChange |
LMKPhotoGridViewController as the browser's LMKPhotoBrowserDataSource / LMKPhotoBrowserDelegate (numberOfPhotos, photo(at:), photoDate(at:), and the other display-index members) |
Ask your own data source (display order follows sortOrder), and reach the presented browser through grid.browser |
LMKKeyboardInsetHelper; LMKKeyboardObserver is internal |
scrollView.lmk_enableKeyboardAdjustment() / lmk_disableKeyboardAdjustment() |
UIView.lmk_fadeIn / lmk_fadeOut |
LMKAnimation.fadeIn(_:) / fadeOut(_:) |
lmk_safeAreaSnp, lmk_setEdgesEqualToSuperview, lmk_centerInSuperview, lmk_setAutoLayoutSize |
plain SnapKit |
UITableView.lmk_configureCellHighlight, UIButton.lmk_animatePress |
cell.lmk_configureCustomHighlight(), LMKAnimation.animateButtonPress(_:) |
lmk_configureIconListRow, lmk_emptyStateCell |
cell.lmk_applyListRow(LMKListRowConfiguration(...)); host an LMKEmptyStateView yourself |
LMKPhotoEXIFService.extractDate(from: UIImage) and the other UIImage overloads |
LMKPhotoMetadata.read(from:) on Data, a file URL, a PHPickerResult, or an NSItemProvider (a decoded image has no metadata left to read) |
LMKDateFormatterHelper.dateFormatter(...), configure(dateFormat:) |
LMKDateFormat.string(_:date:time:), LMKDateFormat.preferredDatePattern |
LMKNavigationBar.setLeftItemEnabled(at:) / setRightItemEnabled(at:), pinToTop(of:) |
updateItem(_:_:) by identifier, install(in:) |
LMKBottomSheetViewController.containerBottomConstraint, refreshSheetColors() |
additionalBottomInset, applyTheme(_:) |
LMKConcurrency.encode / decode returning optionals |
the same names throw; try? keeps the optional |
LMKPhotoBrowserCell, LMKCountdownConfirmationViewController, LMKNetworkDetailViewController, LMKNetworkRequestStore, LMKNavigationDirection |
no longer public |
Added
Theming and styling
- A slot on
LMKThemefor every component (theme.button,theme.chip,theme.navigationBar,theme.toast,theme.monthCalendar,theme.photoBrowser,theme.lottieRefreshControl, and the rest),LMKThemeExtensionfor app-defined values,LMKThemeApplyingwithlmk_startApplyingTheme(), andtraitCollection.lmkTheme/traitOverrides.lmkTheme, so a window or subtree can preview a theme without changing the app's. - A
Styleon every component, every field optional (nilmeans the theme decides), withmerging(_:), public structural subviews, and adidApplyStylehook that runs last. Shared pieces:LMKSurfaceStyle(background clear, solid, gradient, blur, or glass; corners square, fixed, capsule, circle, or concentric; solid, dashed, or inset borders; shadows; content insets),LMKControlStateStylefor highlighted, selected, disabled, and focused looks, andUIView.lmk_apply(surface:defaults:). Turn a part off explicitly with.square,LMKBorderStyle.hidden, orLMKShadowSource.hidden. The open base classes (LMKButton,LMKBottomSheetViewController,LMKCardPageViewController,LMKCardPanelViewController,LMKScrollStackViewController,LMKSegmentedPageViewController,LMKTabBarController) callapplyContentTheme(_:), where a subclass styles its own content. Components that play haptics have ahapticsstyle switch. - Shadows and borders set with
lmk_applyShadow(_:)andlmk_applyBorder(...)follow theme, Dark Mode, and contrast changes. - Typography:
LMKTextStyle(h1toh4, body, caption, and small steps,custom(LMKFontSpec)),lmk_apply(_:color:)on labels, text fields, and text views,UILabel.lmk_make(_:text:color:numberOfLines:),LMKTypographyTheme.maximumScale, andfontDesign/headingFontDesignfor SF Rounded, Serif, or Mono on every step or only the headings. Fixed heights became Dynamic Type minimums. - Tokens:
LMKLayout.symbolMicrothroughsymbolHero,rowHeightCompact/rowHeight/rowHeightComfortable/rowHeightEstimated,readableContentMaxWidth;LMKColor.onFill(_:preferred:)andresolved(_:with:);LMKShadow.Level;LMKAnimation.Spring,Curve, andpressScale.
Controls
LMKButton:RolebyVariant(filled, tinted, outlined, ghost, glass, icon-only) bySize; highlighted, selected, disabled, and focused looks;title,image,setSymbol(_:),isToggle,isLoading,menuwithshowsMenuIndicator,shrinkingTitleToFit(minimumScaleFactor:),minimumHitTarget,Style.animatesSymbolChanges(iOS 26), andinit(title:style:target:action:).- New controls:
LMKCheckbox,LMKRatingControl,LMKCopyableLabel,LMKPhotoButton, andLMKActionTile(withUIColor.lmk_glyphTint(onLightAccentDarkenBy:)).LMKActionTile.Style.glyphMinimumContrastkeeps the glyph readable against the tile in every appearance,titleMinimumScaleFactorshrinks a long title, andminimumHeightsets a floor; at accessibility text sizes a long press shows the tile's title and glyph.LMKPhotoButton.onDropImageaccepts an image dragged onto it on iPad and the Mac. LMKSegmentedControl.Layout(equalWidth,fitContent,scrollable(padding:spacing:)),setItems,insertSegment/removeSegment, andsetEnabled(_:forSegmentAt:).LMKSliderticks andneutralValue(iOS 26),Style.disabled, and a VoiceOver value.LMKTextFieldandLMKTextViewshareLMKTextInputStyle(with disabled and per-LMKValidationStatelooks), pass every delegate call on to your delegate, reportonBeginEditing/onEndEditing, and countmaxCharacterCountin characters; the text field has a themableclearButton, and the text view grows betweenminimumHeightandmaximumHeight.LMKSearchBargainedcancelButtonMode,debounceIntervalwithonDebouncedTextChange, verticalcontentInsets, and a publictextField.- Every LumiKit control keeps a 44pt touch target while enabled; a disabled control absorbs touches inside its bounds, as UIKit's controls do.
Components
LMKStatus(success, warning, error, info, neutral), shared by toasts, banners,LMKStatusLabel, and validation. Toasts:LMKToast.Configuration(title,action, a duration in seconds or persistent,position,presentation,queuePolicy),LMKToast.Handle(dismiss,setMessage), andLMKToast.showUndowith a countdown ring, an optional status or glyph, and a position; only Undo or the countdown ends it unlesstapToDismissis on, so a tap that misses Undo commits nothing.LMKAlert.TextInput.additionalActionsadds buttons after Save (such as a destructive Remove), and a secure text input turns off autocorrection and the other text suggestions.LMKBannerView.show(in:below:insetting:insetsScrollView:): a banner over a screen sits under its navigation bar, caps its width at the readable width, and pushes the scroll view's content down by its height;isFloating, plusStyle.horizontalMargin,verticalMargin,maxWidth, anddismissSymbolPointSize.- Tips:
LMKTipwithLMKTipView.Placement; a pointed tip'sStyle.surfacecolors, borders (solid or dashed), and shadows wrap the bubble and its arrow together, andStyle.arrowColorfills the arrow under a gradient, blur, or glass bubble. A tip is a VoiceOver modal that the escape gesture dismisses. LMKSkeletonView(placeholder shapes),LMKGlassView(variant:)andLMKGlassContainerView,LMKEmptyStateView.ContentwithasContentUnavailableConfiguration()andconfigure(_:animated:),LMKFilterChipBar.SelectionMode,LMKCardViewpresets (cell,elevated,flat,outlined) andonTap, angled and radialLMKGradientViewgradients, anLMKOverscrollFooterViewthat follows its scroll view,LMKChipViewas aUIControlwith a selected look andVariant.tinted,LMKPageIndicatortouch targets and right-to-left layout, andLMKToastView.Style.dismissButton.LMKChipFlowView(theme.chipFlow): chips, or any views, in lines that wrap at the view's width, withStyle.spacingandlineSpacing; its height follows its width, so a self-sizing cell around it resizes.- Lists:
LMKListRowConfiguration(symbol, image, or async image leading; title, subtitle, and detail; disclosure, checkmark, switch, or badge trailing),LMKListRowContentViewwith highlighted and selected looks,lmk_applyListRow(_:),lmk_installRowPointerInteraction(_:),UIListContentConfiguration.lmk_applyTextStyle/lmk_applyLeadingSymbol/lmk_applyThumbnail, andLMKListTable.makeInsetGrouped(). LMKMonthCalendarViewwith subclassableLMKCalendarDayCellandLMKCalendarDayDecoration(dots, badges, glyphs), single, range, and multiple selection, swipe paging that respects Reduce Motion, a today mark that follows the device's date and moves at midnight, and day numerals in the locale's digits. Core addsLMKCalendarDay,LMKCalendarMonth,LMKCalendarSelection, andLMKCalendarSelectionMode: Gregorian dates whatever calendar you supply, with day and month arithmetic that daylight-saving changes cannot shift.- Detail cards:
LMKDetailCard(a header, typed rows: key-value, text, chips, photo strip, progress, navigation, link, rating, image, divider, custom; and actions),LMKDetailCardView(setValue(_:forRowID:),update(rowID:_:)), andLMKDetailPageViewController(cards updated by id, Edit and Share items, Command-E foronEdit,beginEditingwith Command-Return and Escape). LMKMenu: option menus as nativeUIMenus built from sections (singlechoice,multipletoggles that flip while the menu stays open,sortwith a direction,actions,submenu,custom), rebuilt from your state each time the menu opens, and anchored to a bar button, a navigation bar item, or anLMKButton;reloadVisibleMenu(presenting:)repaints an open menu.LMKSortMenuadds a sort menu with a live direction arrow, a layout section, andadditionalSectionsfor filters and commands.LMKTabBarController,LMKTab(lazy roots, asearchrole), andLMKTabBarAppearance:selectTab(identifier:),reorderTabs, badges, Command-1 to Command-9, the iPad sidebar,applyContentTheme(_:), and the iOS 26 minimize behavior and bottom accessory.
Navigation and containers
LMKNavigationBar.Style.appearance(automatic,classic,glass): by default the bar matches the running OS, with Liquid Glass item capsules on iOS 26 and tinted items over a hairline before;Style.itemGlassanditemGlassViewsstyle the capsules.LMKNavigationBarItemhasidentifier,isEnabled,menu,badge, androle(plain, prominent, destructive); the bar addsupdateItem(_:_:),install(in:),subtitle,backgroundContentView, andpinScrollView(_:edgeEffect:). Under the Mac idiom, glyph items and the back button show their label as a tooltip. For system bars:makeBarButtonItem(),UINavigationItem.lmk_setItems(leading:trailing:), andlmk_setSubtitle(_:).LMKNavigationControllerwithLMKPopGestureConfiguring: the swipe back followscanBeginPopGesture, also with the system bar hidden and with the iOS 26 content-area swipe; the top screen sets the status bar style.LMKBottomSheetViewController: drags that hand off to an inner scroll view, Escape and Command-W,contentLayoutGuide,resolveStyle(for:)for subclasses, and VoiceOver modal behavior.LMKActionSheet.Configuration,RowStyle.disabled, and a publicLMKActionSheetRowView;LMKEnumPickersearch and disabled options, sized to its rows;LMKDatePicker.Configuration;LMKCalendarRangeSelectionView.Styleandlocale.LMKCardPageViewController:leadingItem/trailingItem(titled items, roles, badges),usesSystemNavigationBar(pushed onto a stack whose bar shows, the page hands its title and items to that bar), andStyle.showsDragIndicatorfor a page in a sheet.LMKCardPanelViewController.presentation(overlay window or modal),Style.heightRatio, Escape and Command-W, VoiceOver modal behavior, andUIViewController.lmk_cardPanel.LMKScrollStackViewControllerandLMKFormScaffoldwidth modes (inset, readable, capped),makeFieldRow, andmakeHeaderStack;LMKSegmentedPageViewController.segmentedControlPlacementand trackpad swipe paging;LMKProgressViewControllerfinished states,observe(_ progress:), and Escape;LMKErrorHandler.policy.
Core and utilities
LMKDateFormatonDate.FormatStyle:Context(locale, calendar, time zone, hour cycle),string(_:date:time:),intervalString,rangeLabel,residenceLabel,relativeDayString,clockTime,dateWithClockTime,usesTwelveHourClock,widestClockSample,monthYearString,weekdaySymbols,formatter(pattern:), andpreferredDatePatternfor a user-chosen date format.LMKFormatnumber and percent formatting.LMKLogger:noticeandfault; a runtimeminimumLevel;private:on every level for user data (the message stays public unlessmessagePrivacysays otherwise);error:on every level;describe(_:);once(_:_:_:)andresetOnce(_:)for repeating failures;record(_:)for entries another package already wrote;entryHandler,log(_:_:), and theLMKLoggingprotocol withLMKLogger.default; the.lumiKitcategory;LMKLogLevelisComparablewithosLogType;LMKLogEntryisCodableand carriesprivateDetail,file,function, andline;LMKLogStore.formatted()includes the private detail for on-device debugging.LMKErrorHandler.present/confirmRetryandLMKConcurrency.executeTasklog your call site. Image encoding and downsampling, photo metadata, the crop editor, the pick-and-crop coordinator, and presenting a share sheet or picker log a warning when they fail.LMKURLValidator.validate(_:)returnsResult<URL, ValidationError>with the reason; the blocklist covers more private, reserved, and loopback addresses and host spellings;normalizeBaseURL(_:preservingPathExtension:).LMKFile.temporaryURL(extension:),clearTemporaryFiles(olderThan:matchingPrefix:), andclearTemporaryFilesInBackground(olderThan:matchingPrefix:), both returning a count;LMKConcurrency.encode/decodethrow and accept custom coders;onMainActor/onMainActorAfterreturn theirTask;String.lmk_trimmedOrNil;LMKDate.initialize()is optional.LMKImage.downsample(data:maxPixelSize:options:)anddownsample(fileURL:...)(sync and async, with HDR),pixelSize,downsampledJPEG,imageSize, andSymbolOptions(palette, hierarchical, and multicolor rendering, variable value, and the iOS 26 modes).LMKPhotoMetadatareads fromData, a URL, aPHPickerResult, or anNSItemProvider, and writes a date and coordinate.LMKScene.activeWindowScene,keyWindow,presentingViewController,requestClose(_:onError:),configureMacWindow(for:minimumSize:maximumSize:hidesTitleBar:), andobserveGeometry(of:onChange:)(size, safe areas, orientation, and the iOS 26 interactive-resize flag);LMKDevice.observeScreenSize(of:onChange:);LMKTheme.observe(_:)returns a token that ends the observation when released.UIViewController.lmk_formKeyCommands(save:cancel:)with an overridablelmk_cancelFromKeyCommand(),UIView.lmk_pinReadableWidth(in:)andlmk_readableWidthGuide,lmk_displayScale,lmk_forceLayoutDirection(_:),UIScrollView.lmk_disableKeyboardAdjustment(), theUISplitViewController.lmk_setInspector(_:)family, andlmk_apply(_:animatingDifferences:in:)for diffable data sources.LMKMarkdownRenderer.renderandrenderFullcan be called off the main actor.- WCAG contrast helpers on
UIColor:lmk_relativeLuminance(resolvedWith:),lmk_contrastRatio(to:resolvedWith:), andlmk_softestTone(over:washAlpha:minimumContrast:resolvedWith:), the softest tone of a color that keeps a contrast ratio against a wash of itself. LMKShare.Item(image, file, text, url) withtext/urlconveniences;LMKHaptics.isEnabled;UIColor.lmk_composited(over:alpha:)for an opaque tinted surface andlmk_stateShade(by:)for pressed and selected fills;LMKLayout.pixelAligned(_:scale:)andpixelAligned(_:for:)for lines of even thickness.
Photo
- Photo browser:
Stylewiththeme.photoBrowser, a zoom transition fromzoomSourceView, HDR display, an orientation lock on iOS 26,onDismiss, key commands, andstageView. Gestures: a double tap zooms on the tapped point, a zoomed photo pans to its edges and pages past them, a long press plays a Live Photo, and a trackpad pinch on the Mac zooms around the pointer. - Grid:
allowsMultipleSelectionwithselectedIndicesandonSelectionChange(a sideways drag selects a range and scrolls near the edges),contextMenuProvider, prefetch hooks (photoGridPrefetch,photoGridCancelPrefetch),photoGridThumbnail(at:pixelSize:)for thumbnails sized to the cell,browserfor the presented browser, drag and drop with other apps (allowsDraggingPhotos, which carries the photo's original file when the data source implementsphotoGridFileURL(at:), andonDropImages, both off by default), a pinch that changes the column count around the fingers (Style.pinchFeedback), a floating glass toolbar, and the top scroll-edge effect.LMKSinglePhotoViewer.browseranddismiss(completion:). - Crop editor:
aspectRatiosandinitialAspectRatio(LMKCropAspectRatiogains 16:9 and 9:16),onCrop/onCancel, keyboard shortcuts, and a crop frame VoiceOver can move and resize. - Pick-and-crop coordinator:
save(image, metadata) async,onPicked,onCancel,onFailure(Failureis aLocalizedError; with no handler, failures show throughLMKErrorHandler),Strings, andmaximumPixelSize. - Share preview:
detents,onShare/onSave/onFailure/onDismiss, andisSaving; Save Image shows only when the app declaresNSPhotoLibraryAddUsageDescription, unlessStyle.showsSaveButtonsays otherwise. LMKPhotoMetadata.writekeeps the image data intact (every frame and gain map) and writes EXIF dates with their time-zone offset;readhonors them.
Debug
LMKNetworkLogger.Configuration: credential redaction on by default for the common auth, cookie, and API-key headers (redactedHeaderFields) and query items (redactedQueryItems), applied to the URL, aLocationheader, and a URL password;hostFilter, body caps, andrecordsDidChangeNotification;redact(_:configuration:)andrecord(id:).- Request bodies are captured (
isBodyTruncatedsays when the cap cut one), logged requests keep the app's cookies, cache, and credentials, and redirects go back to the app's session, which decides whether to follow them; both hops are recorded. While logging is on, a refused redirect completes about half a second later. LMKNetworkRequestRecord.outcome(pending,success,redirect,error) andisRedirect, next toisSuccessandisError.LMKNetworkHistoryViewControllerlists records with an icon per outcome and an empty state, and its localized detail screen copies to a pasteboard entry that expires. The whole product compiles only underLMK_ENABLE_NETWORK_LOGGING.
Lottie
- The bundled ring, tinted from
theme.lottieRefreshControl;Style(pullThreshold,timeline,minimumSpinDuration,size, tint);Timeline(animation:)for your own animation (split at aPHASE2_SPIN_LOOPmarker, or looped whole);install(on:style:onRefresh:); andmakeRefreshKeyCommand(action:)for Command-R. The ring follows appearance, contrast, and Reduce Motion changes.
Platform
- iOS 26 APIs with fallbacks for iOS 18: Liquid Glass surfaces and glass buttons, concentric corners, scroll-edge effects, tab bar minimize behavior and bottom accessory,
UISearchTab, navigation subtitles, prominent bar items with badges, slider ticks and neutral value, symbol transitions and rendering modes, the split view inspector column, HDR headroom, right-to-left natural alignment, interactive resizing, orientation lock, and background extension views. docs/PLATFORM.md lists each one and the iOS 27 follow-ups. - Mac Catalyst: every component works under the Mac idiom; there the Lottie refresh control returns
nilfrominstalland offers Command-R instead.
Documentation and tooling
- DocC documentation for all five products, docs/MIGRATION-1.0.md, docs/PLATFORM.md, CONTRIBUTING.md, and SECURITY.md.
Scripts/migrate-1.0.shrenames types, members, and call shapes in your app and reports, by file and line, the edits to make by hand;--dry-runshows the changes without writing them.- An Example app with a page for every component, a catalog search, a theme switcher, and a scripted accessibility check (truncation, clipping, overlap, touch targets, labels, contrast, and fixed fonts) across every page.
Changed
Defaults an app upgrading from 0.12 will see differently.
LMKDevice.screenSizeclassifies by the window's portrait width and size classes instead of the screen height: up to 375pt is.compact, up to 402pt.regular, wider.large, and only a window regular in both size classes is.extraLarge. iPhone mini, X, and 11 Pro now read.compact, iPhone 14 Pro through 17 Pro read.regular, and an iPad in Slide Over or a narrow split is no longer.extraLarge.LMKSpacing.cardPaddingandcellPaddingVerticalfollow: iPads step by window size instead of all getting the large values, and narrow windows get phone spacing.- On iOS 26,
LMKNavigationBardraws its items on Liquid Glass with no hairline, like the system bar.theme.navigationBar.appearance = .classickeeps the 0.12 look. LMKButton: every button now shrinks slightly and plays a light haptic when pressed (0.12 did this only for buttons given a style);Style.pressAnimationandStyle.hapticsturn them off. A button that opens itsmenuon tap does not shrink. WhileisLoadingis on, taps no longer fall through to the views behind it, VoiceOver still reads the title, and an icon-only button keeps its width. Under the Mac idiom, buttons keep their iOS look instead of drawing as bare titles.- Chips: a selected filled
LMKChipViewshows a darker fill instead of switching to an outline (Style.selectedVariant = .outlinedkeeps the 0.12 swap), and a tappable chip shows a pressed look. AnLMKFilterChipBarwith a filled chip style draws unselected chips in a soft tint and the selected chip fully filled. LMKBannerViewhas an opaque tinted background with a thin border in the status color, larger corners, and a smaller dismiss glyph, in place of 0.12's translucent tint.LMKPhotoBrowserViewControllerandLMKPhotoCropViewControllerpresent full screen on their own (0.12 showed them as a sheet when presented directly). The browser presents over the screen it came from, so a dismiss drag reveals that screen; setmodalPresentationStyle = .fullScreenfor a black backdrop. A dismiss drag moves the photo with the finger at full size and fades only the background, where 0.12 shrank the photo and faded the whole browser.- In a regular-width window (iPad, Mac),
LMKBottomSheetViewControllerand the action sheet and enum picker built on it are capped atStyle.maxWidth(the readable width by default) and centered, instead of spanning the whole window. LMKTipViewkeeps its bubble inside the safe area, and.automaticplacement picks above or below by the room on each side. VoiceOver reads the tap-anywhere area as "Got it, button".LMKLoadingStateViewstays hidden untilstartLoading(); an inline view without a set height sizes to its content.LMKBadgeViewkeeps its own size in stack views.lmk_makeCircular()keeps a view round as its size changes. Views insideLMKGradientVieware reachable by VoiceOver.LMKSwitch's thumb usesLMKColor.onAccentinstead of pure white;Style.thumbTint = .whitekeeps the 0.12 thumb.- With no value given,
LMKColorTheme.primaryVariantis an opaque, slightly darker shade ofprimary(0.12'sprimaryDarkdefault was translucent green), andoutlineisdividerat half opacity. Pass both toLMKColorTheme(...)to keep your 0.12 colors. - Small text (
.small,.extraSmall) and the helper text and character counter ofLMKTextFieldandLMKTextViewdefault toLMKColor.textSecondaryinstead oftextTertiary, to meet WCAG AA contrast. - Labels made with
UILabel.lmk_makealign natural text to the interface's layout direction, like a plainUILabel; 0.12's factory labels followed the text's own direction. UIColor(lmk_hex:)returnsnilfor a string with any non-hex character; 0.12 parsed the valid leading part.LMKLogEntry.messageholds only the logged text, with the call site infile,function, andline;formattedMessagegives the 0.12 string, andLMKLogStore.formatted()still includes the call site.LMKNetworkRequestRecord.isErrorno longer counts a 3xx response: 304 Not Modified isisSuccess, other 3xx responses areisRedirect, and a record with a transport error isisErrorand neverisSuccess.displayDurationfollows the locale ("2,500ms").
Fixed
Bugs present in 0.12.
Controls and text input
LMKSwitchandLMKSegmentedControlignoredisEnabled: a disabled control looked the same and still changed value.LMKSwitch.setOn(_:animated:)never animated.- A vertical scroll that started on an
LMKSegmentedControlwas taken by its indicator drag, and taps near its edges did nothing. A segmented control sized to its titles could collapse a segment at large text sizes. LMKSlidercrashed in Mac Catalyst apps that use the Mac idiom.LMKSlider,LMKSearchBar, theLMKCardPageViewControllerheader buttons, and theLMKSharePreviewViewControllerclose button had touch targets smaller than 44pt.LMKPageIndicatorhad no VoiceOver label.- A display-only
LMKChipViewswallowed taps meant for the view behind it, such as a table cell. Outlined chip borders could look thicker on some edges than on others on 3x screens. LMKTextFieldandLMKTextViewcountedmaxCharacterCountin UTF-16 units, so an emoji used up several characters; they blocked input-method composition near the limit and rejected an over-long paste instead of keeping what fit.LMKTextField's clear button never appeared, andLMKTextViewhelper text could wrap one word per line.
Sheets, panels, and navigation
- Bottom sheets: a tall sheet lifted by the keyboard could rise above the top safe area, a sheet presented while the keyboard was up sat behind it, and a dismissed sheet could not be presented again. Rows in a multi-page
LMKActionSheetstayed tappable during the page slide, so a tap could run an action from the wrong page.LMKEnumPickerrows had a fixed height and clipped at large text sizes.LMKDatePicker.presentRangeleaked its pickers. LMKCardPanelViewControllerignored safe areas, did not give key-window status back to the window underneath after an overlay panel closed, and dropped touches meant for a controller the panel presented.LMKCardPageViewControllerdrew its header under the status bar, or under the Mac window controls, when the page filled the screen, and settingtitleafter the view loaded did not update the header.LMKNavigationBarignored the left and right safe-area insets, so in landscape its items could sit under the sensor housing.LMKScrollStackViewControllerandLMKFormScaffoldmeasured their content from the screen edges, so on iOS 26 content slid under a floating tab bar or split view sidebar, and in landscape under the sensor housing.LMKSegmentedPageViewController: tapping a segment during a page slide moved the control without changing the page, the page swipe fired alongside sliders and the indicator drag,setPage(_:animated:)before the view loaded was ignored, and the first page was laid out at zero width. Under the Mac idiom the segmented control did not show; it now sits in the window toolbar.- Under the Mac idiom, an
LMKNavigationControllerwith a visible bar lost its toolbar back button after a full-screen presentation over it closed, and after a presented split view closed its back button and title sat a sidebar's width in from the window controls. lmk_topViewControllerrecursed until it crashed on an empty navigation controller, andlmk_presentAlertOnTopre-centered an action sheet popover that was already anchored to a view.
Feedback and status
- A banner shown over a screen covered the content without making room for it and let the content show through, and with Reduce Motion on a dismissed banner shown again stayed invisible.
- A pointed tip stayed where it first appeared after a rotation or resize, and
dismiss()could callonDismisstwice. LMKFloatingButtonkept its old position when the window rotated or resized, and a drag could leave it under a side safe-area inset.LMKEmptyStateViewcould show its message one word per line when first laid out at zero width, as a table background is.LMKLoadingStateViewlogged a broken constraint as a table background.LMKAlert.presentCountdownConfirmationwith a long message grew taller than the screen and pushed its buttons out of reach.- In right-to-left layouts an overflowing
LMKFilterChipBaropened on its last chips.LMKPageIndicatorcrashed on a negative page count. The skeleton shimmer stopped for good after the app went to the background.
Photo
- Photo browser: a zoomed photo could be panned past its edges, and a double tap zoomed on a different point than the one tapped. A dismiss drag faded the photo along with the background. A swipe made while the browser was opening was undone, and the browser jumped back to its first photo whenever it reappeared. Rotating or resizing left the pages misaligned. At 1x, the first sideways drag on a narrow photo moved the photo instead of paging. Releasing a pinch past the maximum zoom brought the controls back over the photo, and the LIVE badge stayed when the other controls hid. In right-to-left layouts the pages and arrow keys ran the wrong way, and on the Mac paging while zoomed left the controls hidden.
- Photo grid cells held full-size images, which used a lot of memory.
- Crop editor: it made a full-size copy of the photo before cropping, could lose a locked aspect ratio, and took a pinch only with both fingers outside the crop frame. Pick-and-crop could fail to show the crop editor when the photo loaded before the picker finished closing.
- Share preview: tapping Save twice saved twice; Save in an app without
NSPhotoLibraryAddUsageDescriptionwent to the photo library anyway (which terminates the app) instead of reporting.photoLibraryAccessDenied; and a failed write that came back without an error was neither logged nor reported.
Core and utilities
- Reading
LMKDevice.deviceType,isIPad, orisMacCatalystoff the main thread crashed, andLMKDevice.hasTopNotchreturnedtrueon iPads. LMKDate.calendarand the helpers built on it (today,isToday,startOfDay) kept the launch time zone after a time-zone change unless the app had calledLMKDate.initialize().LMKURLValidatoraccepted loopback and private addresses written in shorthand, octal, or hex IPv4 form (127.1,0177.0.0.1), and*.localhosthosts.UIScrollView.lmk_enableKeyboardAdjustment()scrolled a focused text view that was itself the adjusted scroll view, and left extra space above the keyboard.LMKMarkdownRenderercould style the wrong line as a heading, and missed tables in text with Windows (CRLF) line endings.- Under Reduce Motion,
LMKAnimation.animateErrorShakeremoved the view's own border. - A cell using
lmk_applyCustomHighlight(highlighted:animated:)could lose its highlight when pressed again during the un-highlight. LMKShadowStylehad no public initializer.- Calling
LMKLogger.configure(subsystem:)while other threads were logging was a data race.
Debug and Lottie
- With network logging on, request bodies were never recorded, redirects were followed without the app's session seeing them, and logged requests dropped the app's cookies, cache, and credentials. Sessions set up with
lmk_enableNetworkLogging()kept being intercepted afterLMKNetworkLogger.disable(). LMKLottieRefreshControlstarted a refresh after the pull went back below the threshold, or on a bounce or a programmatic scroll, and showed a still ring when UIKit started the refresh before the finger lifted.
Deferred
Codable themes, a LumiKitSwiftUI product, video in the photo browser, and the iOS 27 APIs (listed in docs/PLATFORM.md) are 1.x work.